docs(website): sync dev docs from e30c938bb1

This commit is contained in:
github-actions[bot]
2026-10-14 00:29:22 +00:00
parent 74ad23b053
commit a656d6ac3f
5 changed files with 147 additions and 19 deletions
+3 -3
View File
@@ -1,8 +1,8 @@
snapshots:
dev:
source-ref: 90161fbd7d87b6c07b88183b801b30b4925d84d7
source-sha: 90161fbd7d87b6c07b88183b801b30b4925d84d7
version: 0.1.5.dev3
source-ref: e30c938bb159322d5018b9c261c001c658f40071
source-sha: e30c938bb159322d5018b9c261c001c658f40071
version: 0.1.5.dev5
latest:
source-ref: 5a7728c304b5528e954e9ae900cf21f84e1a12b1
source-sha: 5a7728c304b5528e954e9ae900cf21f84e1a12b1
@@ -163,6 +163,13 @@ metrics_bind_address = "0.0.0.0:9090"
log_level = "info"
# Connection ceiling for the gateway's database pool. Omit to use the backend
# default: 10 for Postgres, 5 for on-disk SQLite.
database_max_connections = 10
# Connection ceiling for the Postgres mutation lock pool. Omit to use 4.
database_lock_max_connections = 4
# When omitted, the gateway auto-detects Kubernetes, then Podman, then Docker.
# VM is never auto-detected and requires an explicit entry here.
compute_driver = "kubernetes"
@@ -329,6 +336,26 @@ External compute drivers declare this requirement by adding `openshell.compute.l
`[openshell.gateway.auth] allow_unauthenticated_users = true` is an unsafe local-development and trusted-proxy escape hatch. It accepts user-facing CLI/API calls without OIDC or mTLS credentials while sandbox supervisors still authenticate with gateway-minted sandbox JWTs. Leave it false for shared and production gateways.
## Database Pool
`[openshell.gateway] database_max_connections` sets the connection ceiling for the gateway's database pool. One pool is shared by every database-backed RPC, so this value is also the gateway's ceiling on concurrent database work. When the pool is saturated, callers queue on connection acquisition and sqlx logs that the time to acquire a connection exceeded its slow threshold. The background readiness monitor draws from the same pool, so sustained saturation can push its database ping past the check timeout and flip `/readyz` to not-ready while the gateway is otherwise serving.
Omit the key to use the backend default, which is unchanged from earlier releases:
| Backend | Default ceiling |
|---|---|
| Postgres | 10 |
| SQLite (on disk) | 5 |
| SQLite (in memory) | 1, always. The database lives inside its single connection, so a configured value is ignored and the gateway logs that it was. |
The equivalent flag and environment variable are `--db-max-connections` and `OPENSHELL_DB_MAX_CONNECTIONS`, and Helm deployments set the key under `gatewayConfig.openshell.gateway`. Values below `1` are rejected at startup: a zero-sized pool would block every acquisition rather than degrade. Postgres needs at least `2`, because the SSH identity lock keeps one data connection while it queries the pool, so the gateway rejects `1` there too.
On Postgres, the gateway holds its mutation advisory locks on a separate lock pool, one connection per guarded mutation while it runs or waits. `[openshell.gateway] database_lock_max_connections` sets that pool's ceiling, `4` by default, so it bounds how many guarded mutations a replica runs or waits on in Postgres at once. When every lock connection is in use, further guarded mutations queue for one, and fail with `MUTATION_LOCK_TIMEOUT` only if none frees up before their 10-second lock wait runs out. The equivalent flag and environment variable are `--db-lock-max-connections` and `OPENSHELL_DB_LOCK_MAX_CONNECTIONS`, and Helm deployments set the key under `gatewayConfig.openshell.gateway`. Values below `1` are rejected at startup. SQLite has no lock pool, so the gateway ignores the key there and logs that it did.
On Postgres, each replica opens both pools, so plan for `(database_max_connections + database_lock_max_connections) × replicaCount` connections, 14 per replica with the defaults, and keep that total below the server's own `max_connections` (and below any connection-pooler limit in front of it). Rollouts run extra pods; refer to [Size PostgreSQL Connections](/kubernetes/high-availability#size-postgresql-connections). Raising either ceiling past what the server admits moves the failure from a slow acquire to a refused connection.
On SQLite, writers are serialized regardless of pool size, so a larger ceiling buys read concurrency only. A gateway that needs more write throughput wants an external Postgres database (`server.externalDbSecret`), not a larger SQLite pool.
## OCSF JSONL Output
`[openshell.gateway.ocsf_log]` enables a gateway-local file containing native OCSF events emitted by the gateway, including events without a sandbox association. Omit the table to disable it. Restart the gateway after changing this configuration.
+100 -14
View File
@@ -212,6 +212,9 @@ configuration, and access control.
| Relay rejections | `openshell_server_relay_rejected_total` | Alert on any increase. |
| Relay claims | `openshell_server_relay_claim_duration_seconds`, `openshell_server_relay_expired_total` | Supervisor connect-back time for exec, SSH, forwarding, and service traffic, including relays requested through peers. A high 99th percentile while rejections stay flat points at the supervisor or its node, not at relay capacity. Relays not claimed within 10 seconds are missing from the histogram and count as expired, so alert on any increase. |
| Routed requests | `openshell_server_routed_request_attempts_total`, `openshell_server_peer_request_duration_seconds` | Local/peer relay setup mix and outbound peer rate, failures, and latency. Counts completed or cancelled attempts, including retries. Spikes of `grpc_code="unavailable"` during rollouts are expected. Do not use as an HPA target. |
| Lock contention | `openshell_server_mutation_lock_wait_seconds`, `openshell_server_mutation_lock_hold_seconds`, `openshell_server_mutation_lock_timeouts_total` | Alert on any timeout. Long holds on a scope make its waiters wait, and holds longer than 10 seconds make them time out. |
| Lock errors | `openshell_server_mutation_lock_errors_total` | Alert on any increase, separately from timeouts. PostgreSQL ran out of connection slots, restarted, or became unreachable. Readiness can stay healthy, because it uses established data connections. |
| Lock pool | `openshell_server_mutation_lock_connections_in_use`, `openshell_server_mutation_lock_connections_capacity` | PostgreSQL only. Graph in use over capacity per replica. Values that stay near 1 mean that guarded operations on this replica hold their lock connections, or wait in PostgreSQL for a lock held elsewhere, often by another replica, and lock waits and timeouts follow. |
These queries assume that Prometheus labels each series with `namespace` and
`pod`:
@@ -241,6 +244,24 @@ sum by (operation) (rate(openshell_server_routed_request_attempts_total{route="p
# 99th percentile peer request latency across the fleet.
histogram_quantile(0.99, sum by (le, operation) (rate(openshell_server_peer_request_duration_seconds_bucket{outcome="success"}[5m])))
# 99th percentile mutation lock wait by scope. Watch scope="sandbox" during rollouts.
histogram_quantile(0.99, sum by (le, scope) (rate(openshell_server_mutation_lock_wait_seconds_bucket[5m])))
# Mutation lock timeouts by scope. Alert on any increase.
sum by (scope) (increase(openshell_server_mutation_lock_timeouts_total[10m]))
# 99th percentile time a mutation lock is held, by replica and scope.
# Conflicting acquisitions time out after 10 seconds.
histogram_quantile(0.99, sum by (namespace, pod, le, scope) (rate(openshell_server_mutation_lock_hold_seconds_bucket[5m])))
# Mutation lock acquisitions that failed without timing out. Timeouts do not
# include them, so alert on any increase here as well.
sum by (namespace, pod, scope) (increase(openshell_server_mutation_lock_errors_total[10m]))
# Lock-pool connections in use on each replica, PostgreSQL only. Values that
# stay near 1 mean the pool is close to full.
max by (namespace, pod) (max_over_time(openshell_server_mutation_lock_connections_in_use[5m]) / openshell_server_mutation_lock_connections_capacity)
```
Relay capacity is used on the replica that owns the sandbox's supervisor
@@ -250,6 +271,20 @@ stays counted for up to about 40 seconds, the 10-second claim timeout plus the
30-second cleanup interval. Routed request counts include retries during
rollouts.
Each supervisor that reconnects takes one or two short sandbox-scoped locks on
the replica that receives it. When a gateway pod stops during a rollout, its
supervisors reconnect to the other replicas, so watch
`openshell_server_mutation_lock_wait_seconds` and
`openshell_server_mutation_lock_timeouts_total` with `scope="sandbox"` on the
receiving replicas. Rising waits or any timeout mean that those replicas
receive reconnects faster than they can absorb them, or that slow compute
driver, credential backend, middleware, or provider profile source calls during
sandbox and provider changes hold their locks longer. Guarded work has no time
limit of its own, so such a call holds its lock until it returns:
`openshell_server_mutation_lock_hold_seconds` shows how long, and the replica
logs `mutation lock held longer than the lock wait timeout` when it releases a
lock held longer than 10 seconds.
## Scale the Gateway
Scale the gateway by changing a fixed replica count or by letting a
@@ -380,9 +415,14 @@ kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces/openshell/pods
### Size PostgreSQL Connections
Each gateway pod opens up to 10 PostgreSQL connections on demand. The chart
uses the Kubernetes default rolling update strategy for each workload kind, so
the number of pods that run during a rollout depends on the kind.
With the default settings, each gateway pod opens up to 14 PostgreSQL
connections on demand: 10 for data access (`database_max_connections`) and 4
for mutation locks (`database_lock_max_connections`), both set under
`gatewayConfig.openshell.gateway`. A request cancelled during
a lock wait can leave its lock session open for up to 10 seconds, so keep
headroom. The chart uses the Kubernetes default rolling update strategy for
each workload kind, so the number of pods that run during a rollout depends on
the kind.
A Deployment rollout adds up to 25 percent of the replica count, rounded up,
as surge pods. Kubernetes does not count terminating pods against that surge,
@@ -390,18 +430,20 @@ and each replaced pod can keep its connections throughout its termination
grace period. A rollout can therefore run up to twice the replica count at
once, and an eviction or an autoscaler scale-down during the rollout adds more
terminating pods. Set PostgreSQL `max_connections` to at least
`(2 × replicas + surge) × 10`, which leaves one surge of margin for those
pods, plus headroom for your other clients and administration. Use
`autoscaling.maxReplicas` as the replica count when autoscaling is enabled.
`(2 × replicas + surge) × (data + lock connections)`, which leaves one surge
of margin for those pods, plus headroom for your other clients and
administration. Use `autoscaling.maxReplicas` as the replica count when
autoscaling is enabled. With the defaults, that is
`(2 × replicas + surge) × 14`:
| Replicas | Surge | Minimum `max_connections` |
|---|---|---|
| 2 | 1 | `(4 + 1) × 10 = 50` |
| 3 | 1 | `(6 + 1) × 10 = 70` |
| 4 | 1 | `(8 + 1) × 10 = 90` |
| 6 | 2 | `(12 + 2) × 10 = 140` |
| 2 | 1 | `(4 + 1) × 14 = 70` |
| 3 | 1 | `(6 + 1) × 14 = 98` |
| 4 | 1 | `(8 + 1) × 14 = 126` |
| 6 | 2 | `(12 + 2) × 14 = 196` |
With a Deployment, five or more replicas exceed what the PostgreSQL default of
With a Deployment, three or more replicas exceed what the PostgreSQL default of
100 leaves for the gateway, because PostgreSQL reserves 3 connections for
superusers. Raise `max_connections` or use a larger managed instance. Let one
rollout finish before you start another, because each overlapping rollout adds
@@ -410,14 +452,58 @@ its own terminating pods.
If you run several replicas as a StatefulSet with
`workload.allowMultiReplicaStatefulSet`, a rollout adds no surge pods. It
replaces one pod at a time and creates each replacement only after the old pod
exits. Set `max_connections` to at least `replicas × 10`, using
`autoscaling.maxReplicas` when autoscaling is enabled, plus headroom for your
other clients and administration.
exits. Set `max_connections` to at least
`replicas × (data + lock connections)`, 14 per replica with the defaults,
using `autoscaling.maxReplicas` when autoscaling is enabled, plus headroom for
your other clients and administration.
Raise `database_lock_max_connections` only when mutation lock timeouts on a
replica come from a full lock pool: the gateway logs them with the detail
`waiting for a mutation lock connection`, and
`openshell_server_mutation_lock_connections_in_use` stays at
`openshell_server_mutation_lock_connections_capacity`. Also check that the
replica's own acquisitions are not mostly waiting behind another holder: few
of its timeouts carry the detail `waiting for a PostgreSQL advisory lock` or
`canceling statement due to lock timeout`, and `pg_locks` shows the pod's lock
sessions granted rather than waiting. Otherwise, find the slow holder first,
because a larger pool only queues more waiters behind it. Each extra lock
connection adds `2 × replicas + surge` to the minimum `max_connections` for a
Deployment.
If a connection pooler sits between the gateway and PostgreSQL, use session
pooling. The gateway holds session-level advisory locks, which transaction
pooling breaks.
Mutations lock only what they change. Sandbox operations lock their own
sandbox, provider and workspace-profile changes lock their workspace, and
gateway-global policy, settings, and platform-profile changes lock the whole
fleet. Sandbox start, stop, and delete lock only on the replica that runs them,
and only a gateway-global change on that replica blocks them directly.
Provider, workspace-profile, and other replicas' gateway-global changes delay
them only while another operation on the same sandbox waits behind the change.
Sandbox create, start, restart, and delete also take one fleet-wide lock while
they provision or remove the sandbox's SSH host identity, so that step runs for
one sandbox at a time across all replicas. For other
mutations, a lock wait longer than 10 seconds returns `UNAVAILABLE` with the
reason `MUTATION_LOCK_TIMEOUT`, and clients can retry the request. A request
that carried a `request_id` leaves its admission unresolved, like any other
error, so a retry with the same ID returns `REQUEST_OUTCOME_UNCERTAIN`. Observe
resource state and reconcile effects, then send a new request with a new
`request_id`. Refer to
[Durable Request Admission](/sdk/api-errors#durable-request-admission).
### Upgrade from an Earlier Release
With the default settings, each gateway pod now opens up to 14 PostgreSQL
connections instead of 10. The upgrade rollout already runs new pods at that
count, so raise `max_connections` to the minimum in
[Size PostgreSQL Connections](#size-postgresql-connections) before you upgrade.
While gateways from the earlier release and the new release run together
during the upgrade, the older replicas serialize every mutation across the
fleet. Mutations can be slower and can briefly fail under load until the
rollout finishes.
## Next Steps
- To expose the gateway through a highly available data path, refer to
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: Apache-2.0
title: "Gateway Metrics"
sidebar-title: "Gateway Metrics"
description: "Scrape Prometheus metrics from the OpenShell gateway, including supervisor session, relay capacity, and peer routing signals for multi-replica deployments."
description: "Scrape Prometheus metrics from the OpenShell gateway, including supervisor session, relay capacity, peer routing, and mutation lock signals for multi-replica deployments."
keywords: "Generative AI, Cybersecurity, Observability, Metrics, Prometheus, Gateway, Kubernetes, High Availability"
---
@@ -175,6 +175,17 @@ that owns a sandbox's supervisor session:
| `openshell_server_routed_request_attempts_total` | Counter | `operation`, `route`, `relay_kind`, `outcome`, `grpc_code` | Local relay setup and outbound peer attempts, counted once when they finish or are cancelled. Each retry counts separately. |
| `openshell_server_peer_request_duration_seconds` | Histogram | `operation`, `outcome` | Latency of outbound peer requests only (`route="peer"`). For relays, until the owner's supervisor claimed the relay. |
Mutation locks:
| Metric | Type | Labels | Description |
|---|---|---|---|
| `openshell_server_mutation_lock_wait_seconds` | Histogram | `scope` | Time to acquire a mutation lock, recorded for successful acquisitions. |
| `openshell_server_mutation_lock_hold_seconds` | Histogram | `scope` | Time a mutation lock was held, recorded when it is released, including requests cancelled while they held it. A conflicting acquisition times out after 10 seconds, so the gateway also logs `mutation lock held longer than the lock wait timeout` with `scope` and `held_ms` when it releases a lock held longer than that. |
| `openshell_server_mutation_lock_timeouts_total` | Counter | `scope` | Acquisitions that waited longer than 10 seconds, including background work such as supervisor disconnect handling and provider credential refresh. API requests fail with `UNAVAILABLE` and reason `MUTATION_LOCK_TIMEOUT`, and [API Errors](/sdk/api-errors) describes how to retry them. A lock connection that PostgreSQL does not open, although at least a second of the wait remained, counts in `openshell_server_mutation_lock_errors_total` instead. |
| `openshell_server_mutation_lock_errors_total` | Counter | `scope` | Acquisitions that failed without timing out: PostgreSQL did not open a lock connection, or a lock statement failed. API requests fail with `INTERNAL`. |
| `openshell_server_mutation_lock_connections_in_use` | Gauge | None | PostgreSQL lock-pool connections checked out on this replica: holding mutation locks, waiting for a PostgreSQL lock, or being returned to the pool, which can take up to 5 seconds. Exported only with PostgreSQL. |
| `openshell_server_mutation_lock_connections_capacity` | Gauge | None | Lock-pool size per replica, `4` unless [`database_lock_max_connections`](/how-it-works/gateways/configuration#database-pool) sets it. Exported only with PostgreSQL. |
The labels take these values:
- `operation` is `relay`, `report_provider_readiness`, `report_endpoint_status`,
@@ -210,9 +221,12 @@ The labels take these values:
unchanged. The label is not named `code` because
`openshell_server_grpc_requests_total` uses that name for the numeric status
code.
- `scope` is `global`, `workspace`, or `sandbox`.
Gauges and counters exist from startup. They start at `0`, except
`openshell_server_relay_pending_capacity`, which starts at its limit. `openshell_server_routed_request_attempts_total` starts with seven
`openshell_server_relay_pending_capacity` and
`openshell_server_mutation_lock_connections_capacity`, which start at their
limits. `openshell_server_routed_request_attempts_total` starts with seven
`outcome="success", grpc_code="ok"` series: four relay kind and route
combinations, and three other peer operations with `relay_kind="none"`. Other outcomes and codes,
and every histogram, appear after their first sample, so write alert expressions
+1
View File
@@ -31,6 +31,7 @@ Recognized gateway reasons include the following.
| `INVALID_ARGUMENT` | `INVALID_ARGUMENT` | Correct the fields listed in `BadRequest`. |
| `RESOURCE_VERSION_CONFLICT` | `ABORTED` | Read the resource again and construct a new conditional write. `metadata.recovery` is `REFRESH_STATE`; `current_resource_version` is included when known. |
| `PROFILE_SOURCE_UNAVAILABLE` | `UNAVAILABLE` | Retry a profile snapshot read after at least the supplied delay. |
| `MUTATION_LOCK_TIMEOUT` | `UNAVAILABLE` | The request waited more than 10 seconds for a concurrent mutation to release a sandbox, workspace, or gateway-wide lock. Retry after at least the supplied delay. A request that carried a `request_id` leaves its admission unresolved, like any other error, so a retry with that ID returns `REQUEST_OUTCOME_UNCERTAIN`. Observe resource state and reconcile effects before you start a new request with a new ID. |
| `REQUEST_ID_PAYLOAD_MISMATCH` | `FAILED_PRECONDITION` | Keep the original payload for that request ID. Inspect the original operation before submitting a different one. |
| `REQUEST_OUTCOME_UNCERTAIN` | `FAILED_PRECONDITION` | An attempt is admitted but has no confirmed replayable success. Observe resource state and reconcile effects. Do not switch to a new ID to bypass the admission. |
| `REQUEST_REPLAY_UNAVAILABLE` | `FAILED_PRECONDITION` | The original scope, resource, interceptor transformation, or fingerprint key is no longer replayable. Reconcile effects; the gateway does not execute the request again. Missing private-key material can also reject admission before work starts. |