Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
62a3bb8
feat(gcp): query Google Cloud metrics live with PromQL
JeremyFunk Oct 14, 2026
b90893d
test(gcp): live PromQL fan-out, failures, tokens and discovery
JeremyFunk Oct 14, 2026
177072c
fix(gcp): review fixes for live PromQL queries
JeremyFunk Oct 14, 2026
fa70a30
feat(gcp): refuse PromQL answers of more than 500 series
JeremyFunk Oct 14, 2026
767f128
fix(gcp): live PromQL reads only set-up connections, once per metrics…
JeremyFunk Oct 15, 2026
0c36870
fix(gcp): cap only live-query answers, leave the poller's reads uncapped
JeremyFunk Oct 15, 2026
02d1821
Merge branch 'feat/gcp-infra-page' into feat/gcp-promql-query
JeremyFunk Oct 15, 2026
7be72a2
Merge branch 'feat/gcp-infra-page' into feat/gcp-promql-query
JeremyFunk Oct 15, 2026
84d6cf6
Merge branch 'feat/gcp-infra-page' into feat/gcp-promql-query
JeremyFunk Oct 15, 2026
c08f14f
Merge branch 'feat/gcp-infra-page' into feat/gcp-promql-query
JeremyFunk Oct 15, 2026
5347699
Merge branch 'feat/gcp-infra-page' into feat/gcp-promql-query
JeremyFunk Oct 15, 2026
d6cbceb
Merge feat/gcp-infra-page into feat/gcp-promql-query
JeremyFunk Oct 15, 2026
b02cc18
Merge remote-tracking branch 'origin/feat/gcp-infra-page' into HEAD
JeremyFunk Oct 15, 2026
2a8b40b
Merge remote-tracking branch 'origin/feat/gcp-infra-page' into HEAD
JeremyFunk Oct 15, 2026
901ee1e
fix(ci): raise the web bundle budget for #1475
JeremyFunk Oct 15, 2026
093e923
fix(ci): 1 KB headroom on the bundle budget; CI measures the PR merge…
JeremyFunk Oct 15, 2026
92e2d0f
fix(ci): resolve the bundle budget merge
JeremyFunk Oct 15, 2026
0977b2b
Merge branch 'feat/gcp-infra-page' into feat/gcp-promql-query
JeremyFunk Oct 15, 2026
6a5efc7
Merge branch 'feat/gcp-infra-page' into feat/gcp-promql-query
JeremyFunk Oct 15, 2026
f47f9b1
Merge remote-tracking branch 'origin/feat/gcp-infra-page' into feat/g…
JeremyFunk Oct 16, 2026
e9d3883
Merge branch 'feat/gcp-infra-page' into feat/gcp-promql-query
JeremyFunk Oct 16, 2026
233ba63
Merge remote-tracking branch 'origin/feat/gcp-infra-page' into feat/g…
JeremyFunk Oct 16, 2026
a58c95b
Merge branch 'feat/gcp-infra-page' into feat/gcp-promql-query
JeremyFunk Oct 16, 2026
479cea8
Merge remote-tracking branch 'origin/feat/gcp-infra-page' into HEAD
JeremyFunk Oct 16, 2026
e9046f5
Merge commit '83cfdaef66b89823af18133bb54d5186631c4d24' into HEAD
JeremyFunk Oct 16, 2026
92b32e8
Merge branch 'feat/gcp-infra-page' into feat/gcp-promql-query
JeremyFunk Oct 16, 2026
97270f6
Merge branch 'feat/gcp-infra-page' into feat/gcp-promql-query
JeremyFunk Oct 16, 2026
fad4902
Merge branch 'feat/gcp-infra-page' into feat/gcp-promql-query
JeremyFunk Oct 16, 2026
276c536
Merge branch 'feat/gcp-infra-page' into feat/gcp-promql-query
JeremyFunk Oct 16, 2026
e674226
Merge branch 'feat/gcp-infra-page' into feat/gcp-promql-query
JeremyFunk Oct 16, 2026
ab5782c
Merge branch 'feat/gcp-infra-page' into feat/gcp-promql-query
JeremyFunk Oct 16, 2026
6a665cc
Merge branch 'feat/gcp-infra-page' into feat/gcp-promql-query
JeremyFunk Oct 16, 2026
3a5537f
Merge branch 'feat/gcp-infra-page' into feat/gcp-promql-query
JeremyFunk Oct 16, 2026
a276c77
Merge branch 'feat/gcp-infra-page' into feat/gcp-promql-query
JeremyFunk Oct 16, 2026
a011fce
Merge branch 'feat/gcp-infra-page' into feat/gcp-promql-query
JeremyFunk Oct 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/api/src/resources/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,6 @@ export const apiConfiguredEnv = (stage: MapleStage, region: MapleRegion, domains
cloudflareOAuthEnv,
planetScaleOAuthEnv,
optionalPlain("MAPLE_GCP_SERVICE_ACCOUNT_EMAIL"),
// Sign-in for live Google Cloud PromQL queries; unset answers them as unavailable.
optionalSecret("MAPLE_GCP_SERVICE_ACCOUNT_KEY"),
)
22 changes: 22 additions & 0 deletions apps/api/src/routes/internal/integrations.http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {
type TopTrafficGroupDefinition,
} from "@maple/backend/services/integrations/cloudflare-analytics/queries"
import { GcpConnectorService } from "@maple/backend/services/integrations/GcpConnectorService"
import { GcpPromQlService } from "@maple/backend/services/integrations/GcpPromQlService"
import { RailwayMetricsService } from "@maple/backend/services/integrations/RailwayMetricsService"
import { GithubConnectService } from "@maple/backend/services/integrations/vcs/vendor/github/GithubConnectService"
import { VcsCommitService } from "@maple/backend/services/integrations/vcs/VcsCommitService"
Expand Down Expand Up @@ -121,6 +122,7 @@ export const HttpIntegrationsLive = HttpApiBuilder.group(MapleInternalApi, "inte
const cloudflareAnalytics = yield* CloudflareAnalyticsService
const railway = yield* RailwayMetricsService
const gcp = yield* GcpConnectorService
const gcpPromQl = yield* GcpPromQlService
const edgeCache = yield* EdgeCacheService
const env = yield* Env

Expand Down Expand Up @@ -473,6 +475,26 @@ export const HttpIntegrationsLive = HttpApiBuilder.group(MapleInternalApi, "inte
return new GcpResourcesResponse(yield* gcp.resources(tenant.orgId, query))
}),
)
.handle("gcpPromQlQueryRange", ({ payload }) =>
Effect.flatMap(CurrentTenant.Context, (tenant) =>
gcpPromQl.queryRange(tenant.orgId, payload),
),
)
.handle("gcpPromQlQuery", ({ payload }) =>
Effect.flatMap(CurrentTenant.Context, (tenant) =>
gcpPromQl.queryInstant(tenant.orgId, payload),
),
)
.handle("gcpMetricSearch", ({ query }) =>
Effect.flatMap(CurrentTenant.Context, (tenant) =>
gcpPromQl.searchMetrics(tenant.orgId, query),
),
)
.handle("gcpMetricDescribe", ({ query }) =>
Effect.flatMap(CurrentTenant.Context, (tenant) =>
gcpPromQl.describeMetric(tenant.orgId, query),
),
)
.handle("githubStatus", () =>
Effect.gen(function* () {
const tenant = yield* CurrentTenant.Context
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/runtime/service-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { InvestigationService } from "@maple/backend/services/errors/Investigati
import { RecommendationIssueService } from "@maple/backend/services/errors/RecommendationIssueService"
import { CloudflareAnalyticsService } from "@maple/backend/services/integrations/CloudflareAnalyticsService"
import { GcpConnectorService } from "@maple/backend/services/integrations/GcpConnectorService"
import { GcpPromQlService } from "@maple/backend/services/integrations/GcpPromQlService"
import { PlanetScaleConnectionService } from "@maple/backend/services/integrations/PlanetScaleConnectionService"
import { PlanetScaleDiscoveryService } from "@maple/backend/services/integrations/PlanetScaleDiscoveryService"
import { PlanetScaleService } from "@maple/backend/services/integrations/PlanetScaleService"
Expand Down Expand Up @@ -117,6 +118,7 @@ export const HttpServicesLive = Layer.mergeAll(
CloudflareAnalyticsService.layer,
RailwayMetricsService.layer,
GcpConnectorService.layer,
GcpPromQlService.layer,
OrgOpenRouterKeyService.layer,
AuditLogService.layer,
WarehouseQueryService.layer,
Expand Down
11 changes: 4 additions & 7 deletions apps/web/perf/check-bundle-budget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,10 @@ const gzipBytes = chunks.reduce((total, chunk) => total + chunk.gzipBytes, 0)
// schemas on the contract, the connector status rules the sidebar and the
// integrations hub read, and the Infrastructure nav entry with its icon. The
// /infra/gcp pages stay in their route chunks.
// 705.5 KB from the Google Cloud page redesign and Cloud SQL data (#1336, 2026-10-16):
// ~3.8 KB of startup measured in CI on the stack's top at 708.2 against #1480's 704.4 — the
// sparkline, top-statement and database endpoints on the query-engine contract, the Cloud SQL
// settings on the inventory schema, and two lab registry entries. The pages stay in their
// route chunks.
// 706.5 KB with main's 1 KB from #1494, #1500, #1502 merged up the stack.
const maxGzipBytes = 706.5 * 1024
// 702.5 KB from the PromQL query contract (#1475, 2026-10-15), measured in CI at 701.3.
// 706.5 KB with the Google Cloud page redesign (#1336) merged up.
// 707.5 KB with main's 1 KB from #1494, #1500, #1502 merged up the stack.
const maxGzipBytes = 707.5 * 1024
const budgetLabel = `${(maxGzipBytes / 1024).toFixed(1)} KB`

// Anything lazy-only: chat, replay, and every dev-only lab surface. The
Expand Down
52 changes: 4 additions & 48 deletions packages/backend/src/services/integrations/GcpMetricsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
type GcpTimeSeriesQuery,
} from "./gcp/api"
import { gcpSqlSettings } from "./gcp/cloud-sql"
import { describeGcpApiError, RETRIES } from "./gcp/explain"
import {
gcpTopKey,
gcpTopSeries,
Expand Down Expand Up @@ -111,7 +112,6 @@ const halts = (outcome: ReadOutcome) =>

const SYSTEM_USER_ID = Schema.decodeUnknownSync(UserId)("system-gcp-metrics")

const RETRIES = "Maple retries in 5 minutes."
const NOT_STORED = `${RETRIES} Nothing to do.`

/** What helps a scope too large for one read. A project has no parts to connect instead. */
Expand All @@ -120,50 +120,6 @@ const whenTooLarge = (connector: GcpConnectorRow) =>
? "write to support@maple.dev"
: "connect its folders or projects separately"

/** Why a failed call failed and what to do about it, in the customer's terms. */
const explainApiError = (
error: GcpApiError,
connector: GcpConnectorRow,
reading: "metrics" | "resources",
) => {
// The host project is the one Google checks for billing, enabled APIs and quota. A project
// scope is its own host, and reads as "the project".
const host = `${connector.scopeType === "project" ? "project" : "host project"} ${connector.projectId}`
const scope = `${connector.scopeType} ${connector.scopeId}`
if (error.reason === "BILLING_DISABLED") {
return `The ${host} has no active billing account, and ${error.api} only answers for projects that have one. Link one: https://console.cloud.google.com/billing/linkedaccount?project=${connector.projectId} Maple retries every 5 minutes.`
}
if (error.reason === "SERVICE_DISABLED") {
return `The ${error.api} API is switched off in the ${host}. Run the setup script again: it switches the API on.`
}
if (error.kind === "denied" || error.kind === "not_found") {
if (error.api === "IAM Credentials") {
// A connection that has been read before lost its account or its grant.
if (connector.lastMetricsReceivedAt !== null) {
return "Maple can no longer sign in as this connection's read-only service account. The account was deleted, or the grant to Maple was removed or is blocked by an organization policy. Run the setup script again: it restores both, or says what blocks it."
}
// Shown both before the script ever ran and for the minute or two after a run in
// which Google does not accept the new grant yet, so it has to read right in both.
return "Maple can't sign in as this connection's read-only service account yet. After a setup run Google needs a few minutes to accept the new grant, and Maple retries every 5 minutes. If this stays, the account does not exist, or the grant to Maple is missing or blocked by an organization policy: run the setup script and read its last lines."
}
return reading === "metrics"
? `Google denied Maple's read of ${scope}: the read-only roles are missing. Run the setup script again: it grants them. A new grant can take a few minutes.`
: `Google denied the resource listing for ${scope}. Run the setup script again: it grants Cloud Asset Viewer.`
}
// Signing in is counted against Maple's own project, not the host project.
if (error.kind === "rate_limited" && error.api !== "IAM Credentials") {
return `Google rate-limited the ${error.api} API for the ${host}. ${RETRIES} If this repeats, raise that API's quota on the project.`
}
return `Maple's request to Google's ${error.api} API failed. ${RETRIES} If this repeats, write to support@maple.dev.`
}

/**
* What the connection shows for a failed call: one sentence of cause, one of what to do, and
* Google's status at the end.
*/
const describeApiError = (error: GcpApiError, connector: GcpConnectorRow, reading: "metrics" | "resources") =>
`${explainApiError(error, connector, reading)} (${error.message})`

const toPersistenceError = makePersistenceErrorMapper(
IntegrationsPersistenceError,
"Google Cloud metrics database error",
Expand Down Expand Up @@ -435,7 +391,7 @@ export class GcpMetricsService extends Context.Service<GcpMetricsService, GcpMet
Effect.catchTags({
"@maple/api/integrations/GcpApiError": (error) =>
Effect.succeed({
lastResourcesError: describeApiError(error, connector, "resources"),
lastResourcesError: describeGcpApiError(error, connector),
}),
"@maple/http/errors/IntegrationsPersistenceError": () =>
Effect.succeed({
Expand Down Expand Up @@ -626,7 +582,7 @@ export class GcpMetricsService extends Context.Service<GcpMetricsService, GcpMet
? []
: [
firstFailure.error.kind === "rate_limited"
? describeApiError(firstFailure.error, connector, "metrics")
? describeGcpApiError(firstFailure.error, connector)
: `${failed} of ${METRIC_COUNT} metric queries failed, first ${firstFailure.type}. The rest were stored. ${RETRIES} (${firstFailure.error.message})`,
]),
]
Expand Down Expand Up @@ -660,7 +616,7 @@ export class GcpMetricsService extends Context.Service<GcpMetricsService, GcpMet
}),
Effect.catchTags({
"@maple/api/integrations/GcpApiError": (error) =>
recordFailure(connector, describeApiError(error, connector, "metrics")),
recordFailure(connector, describeGcpApiError(error, connector)),
"@maple/api/integrations/GcpMetricsIngestError": (error) =>
error.status === 402
? recordFailure(
Expand Down
Loading
Loading