Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0f7a715
feat(integrations): Google Cloud connector API and setup script
JeremyFunk Oct 8, 2026
b2f50bd
fix(integrations): grant the sink Logs Writer, keep health transition…
JeremyFunk Oct 8, 2026
a2826df
chore(ios): regenerate the OpenAPI spec for the Google Cloud tag
JeremyFunk Oct 8, 2026
bc7d03b
feat(integrations): scope Google Cloud connectors to a project, folde…
JeremyFunk Oct 8, 2026
d1a02e7
Merge branch 'main' of https://github.com/MapleTechLabs/maple into fe…
JeremyFunk Oct 8, 2026
ab4afb0
fix(integrations): make Google Cloud teardown report what it could no…
JeremyFunk Oct 8, 2026
55d33ec
Merge branch 'main' of https://github.com/MapleTechLabs/maple into fe…
JeremyFunk Oct 8, 2026
c17da54
fix(integrations): lock the connector row while its switches change
JeremyFunk Oct 8, 2026
23b03d8
Merge branch 'main' of https://github.com/MapleTechLabs/maple into fe…
JeremyFunk Oct 8, 2026
8dce20f
fix(integrations): never tear down Google Cloud resources on an uncle…
JeremyFunk Oct 8, 2026
8685c6d
fix(integrations): look the Google Cloud reader account up in the lis…
JeremyFunk Oct 8, 2026
5b1ecdb
fix(integrations): forget the last Google Cloud log push when log for…
JeremyFunk Oct 8, 2026
922e8d2
fix(integrations): run the Google Cloud removal lookups without gclou…
JeremyFunk Oct 8, 2026
903f2f0
feat(integrations): Google Cloud setup script that is safe to paste, …
JeremyFunk Oct 8, 2026
851d715
fix(integrations): Google Cloud script remedies for a disabled API, a…
JeremyFunk Oct 9, 2026
eece8d7
fix(integrations): Google Cloud script for callers with fewer rights,…
JeremyFunk Oct 9, 2026
1994bc5
fix(integrations): Google Cloud script findings from a live run
JeremyFunk Oct 9, 2026
7a15a75
fix(integrations): Google Cloud script wording from the final live run
JeremyFunk Oct 9, 2026
570a38d
feat(integrations): leave GKE container logs out of the default Googl…
JeremyFunk Oct 9, 2026
4277d81
fix(integrations): name the clause to delete in the Google Cloud scri…
JeremyFunk Oct 9, 2026
3a0eb50
Merge remote-tracking branch 'origin/main' into feat/gcp-connector-api
JeremyFunk Oct 10, 2026
349fb62
refactor(integrations): Google Cloud connector queries on effect-orm
JeremyFunk Oct 10, 2026
88ad175
Merge remote-tracking branch 'origin/main' into feat/gcp-connector-api
JeremyFunk Oct 14, 2026
000b82f
fix(gcp): follow main's ApiAuthorizationV2Live rename and effectful H…
JeremyFunk Oct 14, 2026
ff85d4b
fix(gcp): harden the setup and cleanup scripts for customer environments
JeremyFunk Oct 14, 2026
9a7d532
Merge branch 'origin/main' into feat/gcp-connector-api
JeremyFunk Oct 15, 2026
5694772
chore(db): regenerate the gcp_connectors migration after main's newer…
JeremyFunk Oct 15, 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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,11 @@ INGEST_REQUIRE_TLS=false
# CLOUDFLARE_OAUTH_REVOKE_URL=https://dash.cloudflare.com/oauth2/revoke
# MAPLE_CLOUDFLARE_API_BASE_URL=https://api.cloudflare.com/client/v4

# Google Cloud integration (the project owner runs a generated gcloud script in Cloud Shell).
# Logs need no configuration. For metrics, set the email of a Google service account Maple owns;
# the setup script lets it impersonate a read-only metrics account in the customer's project.
# MAPLE_GCP_SERVICE_ACCOUNT_EMAIL=

# OpenTelemetry (API self-observability) — read by @maple-dev/effect-sdk
# MAPLE_ENVIRONMENT=local # "local" = no export; any other value enables OTLP
# MAPLE_ENDPOINT=http://127.0.0.1:3474 # Ingest gateway endpoint (enriches with org_id)
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/resources/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,5 @@ export const apiConfiguredEnv = (stage: MapleStage, region: MapleRegion, domains
optionalSecret("GITHUB_APP_WEBHOOK_SECRET"),
cloudflareOAuthEnv,
planetScaleOAuthEnv,
optionalPlain("MAPLE_GCP_SERVICE_ACCOUNT_EMAIL"),
)
376 changes: 376 additions & 0 deletions apps/api/src/routes/v2/integrations-gcp.http.test.ts

Large diffs are not rendered by default.

129 changes: 129 additions & 0 deletions apps/api/src/routes/v2/integrations-gcp.http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { HttpApiBuilder } from "effect/http-api"
import { CurrentTenant } from "@maple/domain/http"
import type { V2GcpConnector } from "@maple/domain/http/v2"
import {
MapleApiV2,
isoTimestamp,
isoTimestampOrNull,
V2InsufficientPermissions,
} from "@maple/domain/http/v2"
import { Effect } from "effect"
import { recordHttpAudit } from "@maple/backend/services/audit/AuditLogService"
import { requireAdmin } from "@maple/backend/services/auth/auth"
import {
GcpConnectorService,
type GcpConnector,
} from "@maple/backend/services/integrations/GcpConnectorService"

// Reading the status is open to every member. Everything else is admin-only: a connector decides
// what may write logs into the org and what Maple reads, and its setup script carries the push
// secret.
const adminOnly = () => V2InsufficientPermissions.make("Only org admins can manage Google Cloud connectors")

const toV2Connector = (connector: GcpConnector): V2GcpConnector => ({
id: connector.id,
object: "gcp_connector",
scope_type: connector.scopeType,
scope_id: connector.scopeId,
project_id: connector.projectId,
logs_enabled: connector.logsEnabled,
metrics_enabled: connector.metricsEnabled,
created_at: isoTimestamp(connector.createdAt),
last_log_received_at: isoTimestampOrNull(connector.lastLogReceivedAt),
last_log_error: connector.lastLogError,
applied_logs_enabled: connector.appliedLogsEnabled,
applied_metrics_enabled: connector.appliedMetricsEnabled,
setup_reported_at: isoTimestampOrNull(connector.setupReportedAt),
})

const auditMetadata = (connector: GcpConnector) => ({
scope_type: connector.scopeType,
scope_id: connector.scopeId,
project_id: connector.projectId,
logs_enabled: connector.logsEnabled,
metrics_enabled: connector.metricsEnabled,
})

export const HttpV2GcpIntegrationsLive = HttpApiBuilder.group(MapleApiV2, "gcpIntegration", (handlers) =>
Effect.gen(function* () {
const gcp = yield* GcpConnectorService

return handlers
.handle("status", () =>
Effect.gen(function* () {
const tenant = yield* CurrentTenant.Context
const status = yield* gcp.status(tenant.orgId)
return {
object: "gcp_integration" as const,
metrics_available: status.metricsAvailable,
connectors: status.connectors.map(toV2Connector),
}
}),
)
.handle("createConnector", ({ payload }) =>
Effect.gen(function* () {
const tenant = yield* CurrentTenant.Context
yield* requireAdmin(tenant.roles, adminOnly)
const connector = yield* gcp.create(tenant.orgId, tenant.userId, {
scopeType: payload.scope_type,
scopeId: payload.scope_id,
// A project hosts its own resources; the contract rejects any other value.
projectId: payload.scope_type === "project" ? payload.scope_id : payload.project_id,
logsEnabled: payload.logs_enabled ?? true,
metricsEnabled: payload.metrics_enabled ?? false,
})
yield* recordHttpAudit("gcp_connector.created", {
resourceId: connector.id,
metadata: auditMetadata(connector),
})
return toV2Connector(connector)
}),
)
.handle("updateConnector", ({ params, payload }) =>
Effect.gen(function* () {
const tenant = yield* CurrentTenant.Context
yield* requireAdmin(tenant.roles, adminOnly)
const connector = yield* gcp.update(tenant.orgId, params.id, {
logsEnabled: payload.logs_enabled,
metricsEnabled: payload.metrics_enabled,
})
yield* recordHttpAudit("gcp_connector.updated", {
resourceId: connector.id,
metadata: auditMetadata(connector),
})
return toV2Connector(connector)
}),
)
.handle("setupScripts", ({ params, payload }) =>
Effect.gen(function* () {
const tenant = yield* CurrentTenant.Context
yield* requireAdmin(tenant.roles, adminOnly)
const scripts = yield* gcp.scripts(tenant.orgId, params.id, {
logFilter: payload.log_filter ?? "keep",
})
return {
object: "gcp_connector.setup_scripts" as const,
setup_script: scripts.setupScript,
cleanup_script: scripts.cleanupScript,
}
}),
)
.handle("deleteConnector", ({ params }) =>
Effect.gen(function* () {
const tenant = yield* CurrentTenant.Context
yield* requireAdmin(tenant.roles, adminOnly)
const deleted = yield* gcp.delete(tenant.orgId, params.id)
yield* recordHttpAudit("gcp_connector.deleted", {
resourceId: params.id,
metadata: auditMetadata(deleted.connector),
})
return {
id: params.id,
object: "gcp_connector" as const,
deleted: true as const,
cleanup_script: deleted.cleanupScript,
}
}),
)
}),
)
4 changes: 4 additions & 0 deletions apps/api/src/routes/v2/v2-test-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ import { HttpV2AttributeMappingsLive } from "./attribute-mappings.http"
import { HttpV2DashboardsLive } from "./dashboards.http"
import { HttpV2IngestKeysLive } from "./ingest-keys.http"
import { HttpV2ChatIntegrationsLive } from "./integrations-chat.http"
import { HttpV2GcpIntegrationsLive } from "./integrations-gcp.http"
import { HttpV2PlanetScaleIntegrationsLive } from "./integrations.http"
import { GcpConnectorService } from "@maple/backend/services/integrations/GcpConnectorService"
import { HttpV2ErrorIssuesLive } from "./error-issues.http"
import { HttpV2AnomaliesLive } from "./anomalies.http"
import { HttpV2InvestigationsLive } from "./investigations.http"
Expand Down Expand Up @@ -155,6 +157,8 @@ const v2GroupLayersExceptOnboardingChecklist = (chatWorkspace: Layer.Layer<ChatW
Layer.provide(Layer.mergeAll(MobileDevicesService.layer, LiveActivitiesService.layer)),
),
HttpV2AgentFeedbackLive.pipe(Layer.provide(AgentFeedbackService.layer)),
// Same, plus the Env every harness provides as well.
HttpV2GcpIntegrationsLive.pipe(Layer.provide(GcpConnectorService.layer)),
// Real reads over whichever `WarehouseQueryService` stub the test provides.
HttpV2SessionReplaysLive.pipe(
Layer.provide(
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/runtime/http-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { V2TransportErrorBoundaryLive } from "@/routes/v2/error-envelope"
import { HttpV2ErrorIssuesLive } from "@/routes/v2/error-issues.http"
import { HttpV2IngestKeysLive } from "@/routes/v2/ingest-keys.http"
import { HttpV2ChatIntegrationsLive } from "@/routes/v2/integrations-chat.http"
import { HttpV2GcpIntegrationsLive } from "@/routes/v2/integrations-gcp.http"
import { HttpV2PlanetScaleIntegrationsLive } from "@/routes/v2/integrations.http"
import { HttpV2InvestigationsLive } from "@/routes/v2/investigations.http"
import { HttpV2AgentFeedbackLive } from "@/routes/v2/agent-feedback.http"
Expand Down Expand Up @@ -163,6 +164,7 @@ const ApiV2Routes = HttpApiBuilder.layer(MapleApiV2).pipe(
HttpV2IngestKeysLive,
HttpV2ChatIntegrationsLive,
HttpV2PlanetScaleIntegrationsLive,
HttpV2GcpIntegrationsLive,
HttpV2ErrorIssuesLive,
HttpV2AttributeMappingsLive,
HttpV2AuditLogLive,
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 @@ -36,6 +36,7 @@ import { IncidentClassifier } from "@maple/backend/services/errors/IncidentClass
import { InvestigationService } from "@maple/backend/services/errors/InvestigationService"
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 { 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 @@ -113,6 +114,7 @@ export const HttpServicesLive = Layer.mergeAll(
DailySpendService.layer,
CloudflareAnalyticsService.layer,
RailwayMetricsService.layer,
GcpConnectorService.layer,
OrgOpenRouterKeyService.layer,
AuditLogService.layer,
WarehouseQueryService.layer,
Expand Down
4 changes: 4 additions & 0 deletions apps/ios/Packages/MapleAPI/Sources/MapleAPI/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -7777,6 +7777,10 @@
"description": "Connect PlanetScale to your organization and manage what Maple collects from it: connection status, organization binding, the metrics service token that enables branch-metrics scraping, the database inventory, webhook setup, query insights, and the lifecycle event timeline.",
"name": "PlanetScale Integration"
},
{
"description": "Connect Google Cloud projects, folders or organizations: register a connector, choose whether it forwards logs and whether Maple may read metrics and resources, fetch the `gcloud` scripts that set it up, and see whether logs are arriving.",
"name": "Google Cloud Integration"
},
{
"description": "Deduplicated errors and alert-backed issues tracked through Maple's triage workflow.",
"name": "Error Issues"
Expand Down
Loading
Loading