feat(server): measure lifecycle operations server-side - #1412
Conversation
The server exported one metric and did not measure itself: sandbox.create.duration is a number the SDK reports to POST /metrics/events, which the server forwards. A deployment whose clients call the REST API directly therefore exports nothing at all, and even when clients do report, the number describes their experience rather than the server's work. Nothing was counted either. SandboxErrorCodes lists around forty failure modes - image pull failed, pod ready timeout, execd start failed - and none of them ever reached a metric, so when creation starts failing the server's telemetry stays silent and only the logs know. Add opensandbox.sandbox.operation.duration and .total, recorded by a decorator on the eight mutating lifecycle handlers. The API boundary is where every runtime converges and where the error code has already been decided, so one seam covers docker and kubernetes alike. Read-only endpoints are left alone. error.code goes on the counter only, keeping the histogram low-cardinality, and passes through a shape check: codes are source constants today, but a future call site interpolating a message into detail["code"] must not be able to turn the attribute unbounded. The decorator cannot affect a request: it re-raises untouched, and a failure inside the recording is logged rather than propagated. functools.wraps is load-bearing here - FastAPI builds the request model from the handler signature, and there is a test pinning that. create is time-to-scheduled, not time-to-ready: POST /sandboxes answers 202 and provisions asynchronously. Instrumenting the provisioning path is a separate decision, noted in the docs and the issue. Fixes opensandbox-group#1408
…n docs/ Per AGENTS.md operations-visible behavior goes in docs/ first, and the published telemetry guide described the SDK histogram as the only thing [otel] exports. Kept to the repo's existing pattern for server config: docs/ links to server/configuration.md rather than duplicating the reference. What docs/ gains is the part a reader cannot infer from a table — that the SDK histogram only exists where clients report it, so a dashboard built on it alone looks empty rather than healthy, and that the server-measured create is time-to-scheduled while the SDK number approximates time-to-ready.
|
Unprompted follow-up: e0f9370 documents this in Codex flagged on my sibling PRs that I kept to the repo's existing pattern for server config — |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e0f9370f4b
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| Note `POST /sandboxes` returns `202 Accepted` and provisions asynchronously, so `create` | ||
| here is **time-to-scheduled, not time-to-ready**. Time-to-ready needs instrumentation in the | ||
| provisioning path, which this does not yet cover. |
There was a problem hiding this comment.
Correct the documented create metric boundary
For current Docker and Kubernetes create paths, this note under-describes what the new metric measures: DockerSandboxService.create_sandbox awaits the provisioning thread's result from _provision_sandbox, and KubernetesSandboxService.create_sandbox explicitly awaits _wait_for_sandbox_ready before returning, so operation.duration{operation="create"} includes readiness/provisioning time rather than only scheduling time. Leaving this text here (and the matching note in docs/guides/sdk-telemetry.md) will steer operators away from the only server-side cold-start signal this change adds.
AGENTS.md reference: server/AGENTS.md:L40-L42
Useful? React with 👍 / 👎.
I wrote that create was time-to-scheduled because POST /sandboxes answers 202. The status code is a convention here, not a description: the handler blocks until the sandbox is provisioned. KubernetesSandboxService.create_sandbox awaits _wait_for_sandbox_ready, so the sample includes pod readiness, and the Docker path awaits the provisioning thread, so it includes container start and egress sidecar readiness. That makes it a usable server-side cold-start signal — the opposite of what the note said, and the reason the note mattered: it steered operators away from the only cold-start metric here that does not depend on clients reporting anything.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 208834f092
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| async def async_wrapper(*args: Any, **kwargs: Any) -> Any: | ||
| started = time.perf_counter() | ||
| try: | ||
| result = await func(*args, **kwargs) |
There was a problem hiding this comment.
Count requests rejected before the handler runs
When FastAPI rejects a lifecycle request during parsing or Pydantic validation, this wrapper is never invoked, so neither operation.duration nor operation.total records an error. For example, an authenticated POST /v1/sandboxes body that fails CreateSandboxRequest validation returns 422 before create_sandbox is called, leaving error-rate dashboards built from opensandbox.sandbox.operation.total undercounting bad create requests; instrumenting at the APIRoute/middleware layer would cover those API-boundary failures too.
Useful? React with 👍 / 👎.
|
You're right and I had this backwards — fixed in 208834f. I inferred "asynchronous" from the
So What I kept is the boundary that is real: the measurement ends when the sandbox is ready, so it excludes whatever the workload does afterwards inside the sandbox. |
… counted too A decorator on the handler cannot see a request FastAPI never delivers. Validation happens before the endpoint is called, so a malformed create body returned 422 with nothing recorded — an error-rate dashboard built on operation.total would have undercounted exactly the failures a caller is most likely to produce. Move recording into an APIRoute subclass and reduce the decorator to a marker that tags the handler with its operation name. The route sees RequestValidationError (counted as HTTP_422), HTTPException, unexpected exceptions, and error statuses returned rather than raised. Unmarked read-only routes get the untouched handler, so they pay nothing. Dropping the wrapper also removes the functools.wraps dependency: nothing stands between FastAPI and the handler signature any more. Two tests guard the wiring rather than the logic, because that is where this breaks silently: one pins the eight marked handlers and that the router really uses the route class, the other pins that a call is recorded exactly once. Verified both fail when the route_class or the marker order is wrong.
|
Correct — fixed in 9ad80dc, and I took your suggested layer. A decorator on the endpoint cannot see a request FastAPI never delivers: validation raises before the handler is called, so a malformed create body returned 422 with nothing recorded. And it undercounts precisely the failures a caller is most likely to produce, which is the worst place to have a blind spot in an error rate. Recording now lives in an
Unmarked read-only routes get the untouched handler back, so they pay nothing. A side benefit: dropping the wrapper removes the Two of the new tests guard the wiring rather than the logic, because that is where this construction fails silently — a marker applied in the wrong decorator order, or a router that forgets Suite is 1327 passing, |
Fixes #1408.
Problem
The server exports one metric and it does not measure the server.
sandbox.create.durationis a number the SDK reports toPOST /metrics/events; the server only forwards it. Two consequences:And nothing was ever counted.
SandboxErrorCodeslists ~40 failure modes (IMAGE_PULL_FAILED,K8S_POD_READY_TIMEOUT,EXECD_START_FAILED, …) and not one reached a metric, so when creation starts failing the server's telemetry stays silent and only the logs know.What this adds
opensandbox.sandbox.operation.durationmsoperation,outcomeopensandbox.sandbox.operation.totaloperation,outcome,error.code(failures only)Recorded by a decorator on the eight mutating lifecycle handlers —
create,update_metadata,delete,pause,resume,renew,create_snapshot,delete_snapshot. Read-only endpoints are deliberately left alone.This makes the existing error taxonomy queryable without inventing a new one:
Design notes
error.codeon the counter only. The histogram keeps two attributes so its bucket series stay cheap; the taxonomy lives on the counter, which is one series per code.detail["code"]would otherwise make the attribute unbounded. Anything not code-shaped becomesOTHER.functools.wrapsis load-bearing: FastAPI builds the request model from the handler signature, andinspect.signaturefollows__wrapped__. There is an explicit test for the signature surviving, since losing it would break routing in a way that is easy to introduce and easy to miss.Nonewhen[otel] enabled = false, exactly like the existing one.Scope, and what I did not decide
POST /sandboxesreturns202 Acceptedand provisions asynchronously, socreatehere is time-to-scheduled, not time-to-ready. Those are different numbers and the second is the one an operator usually wants for cold-start. Measuring it needs instrumentation inside the provisioning path, which is a design question about where the readiness boundary lives — I would rather follow your preference than pick unilaterally. Documented as a known limitation inconfiguration.mdand in the issue.Also not here: a gauge of live sandboxes, and provider-level timings (Kubernetes API calls, image pull). Both are straightforward once the shape above is agreed.
Testing
Full suite 1323 passed (21 new),
ruff checkclean. The suite already exercises these routes end to end, so it also confirms the decorator does not disturb FastAPI's request handling.Covered: sync and async handlers, success,
HTTPExceptionwith a code dict,HTTPExceptionwith a plain-string detail (falls back toHTTP_<status>), unexpected exceptions (UNKNOWN_ERROR), no-op when export is disabled, instrument failures swallowed, and the shape check accepting real codes while rejecting messages, identifiers and over-long values.