-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat(server): measure lifecycle operations server-side #1412
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ferponse
wants to merge
4
commits into
opensandbox-group:main
Choose a base branch
from
ferponse:feat/server-operation-metrics
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 3 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
f54e55d
feat(server): measure lifecycle operations server-side
ferponse e0f9370
docs(server): distinguish SDK-reported from server-measured metrics i…
ferponse 208834f
docs(server): correct what operation.duration{create} measures
ferponse 9ad80dc
fix(server): record operations at the route, so rejected requests are…
ferponse File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
110 changes: 110 additions & 0 deletions
110
server/opensandbox_server/integrations/otel/instrument.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| # Copyright 2026 Alibaba Group Holding Ltd. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| """Server-side instrumentation for lifecycle API handlers.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import functools | ||
| import inspect | ||
| import logging | ||
| import time | ||
| from typing import Any, Callable, Optional | ||
|
|
||
| from fastapi import HTTPException | ||
|
|
||
| from opensandbox_server.integrations.otel.metrics import record_sandbox_operation | ||
| from opensandbox_server.services.constants import SandboxErrorCodes | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| OUTCOME_SUCCESS = "success" | ||
| OUTCOME_ERROR = "error" | ||
|
|
||
|
|
||
| def _error_code_from(exc: BaseException) -> str: | ||
| """Pull the server error code out of a raised exception. | ||
|
|
||
| Handlers raise ``HTTPException`` with ``detail={"code": ..., "message": ...}``. When the | ||
| detail is a plain string, or the exception is not an ``HTTPException`` at all, fall back | ||
| to the generic code so a failure is still counted rather than dropped. | ||
| """ | ||
| if isinstance(exc, HTTPException): | ||
| detail = exc.detail | ||
| if isinstance(detail, dict): | ||
| code = detail.get("code") | ||
| if code is not None: | ||
| return str(code) | ||
| return f"HTTP_{exc.status_code}" | ||
| return SandboxErrorCodes.UNKNOWN_ERROR | ||
|
|
||
|
|
||
| def instrumented_operation(operation: str) -> Callable[[Callable], Callable]: | ||
| """Record duration and outcome of a lifecycle handler. | ||
|
|
||
| Measures the server's own work at the API boundary, which is the one place where every | ||
| runtime converges and where the error code has already been decided. Despite the 202 | ||
| status, ``POST /sandboxes`` blocks until the sandbox is provisioned — Kubernetes awaits | ||
| ``_wait_for_sandbox_ready``, Docker awaits the provisioning thread — so ``create`` is a | ||
| cold-start measurement rather than a scheduling one. | ||
|
|
||
| ``functools.wraps`` is load-bearing: FastAPI builds the request model from the handler | ||
| signature, and ``inspect.signature`` follows ``__wrapped__``, so the route keeps its | ||
| parameters. Never swallows or alters an exception, and never lets a metrics failure | ||
| affect the response. | ||
| """ | ||
|
|
||
| def decorator(func: Callable) -> Callable: | ||
| def _record(started: float, outcome: str, error_code: Optional[str]) -> None: | ||
| # record_sandbox_operation already swallows instrument errors, but this sits on | ||
| # every mutating route: a bug in telemetry must not be able to fail a request. | ||
| try: | ||
| record_sandbox_operation( | ||
| operation=operation, | ||
| duration_ms=(time.perf_counter() - started) * 1000.0, | ||
| outcome=outcome, | ||
| error_code=error_code, | ||
| ) | ||
| except Exception: | ||
| logger.exception("Failed to record %s operation metric", operation) | ||
|
|
||
| if inspect.iscoroutinefunction(func): | ||
|
|
||
| @functools.wraps(func) | ||
| async def async_wrapper(*args: Any, **kwargs: Any) -> Any: | ||
| started = time.perf_counter() | ||
| try: | ||
| result = await func(*args, **kwargs) | ||
| except BaseException as exc: | ||
| _record(started, OUTCOME_ERROR, _error_code_from(exc)) | ||
| raise | ||
| _record(started, OUTCOME_SUCCESS, None) | ||
| return result | ||
|
|
||
| return async_wrapper | ||
|
|
||
| @functools.wraps(func) | ||
| def wrapper(*args: Any, **kwargs: Any) -> Any: | ||
| started = time.perf_counter() | ||
| try: | ||
| result = func(*args, **kwargs) | ||
| except BaseException as exc: | ||
| _record(started, OUTCOME_ERROR, _error_code_from(exc)) | ||
| raise | ||
| _record(started, OUTCOME_SUCCESS, None) | ||
| return result | ||
|
|
||
| return wrapper | ||
|
|
||
| return decorator | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When FastAPI rejects a lifecycle request during parsing or Pydantic validation, this wrapper is never invoked, so neither
operation.durationnoroperation.totalrecords an error. For example, an authenticatedPOST /v1/sandboxesbody that failsCreateSandboxRequestvalidation returns 422 beforecreate_sandboxis called, leaving error-rate dashboards built fromopensandbox.sandbox.operation.totalundercounting bad create requests; instrumenting at theAPIRoute/middleware layer would cover those API-boundary failures too.Useful? React with 👍 / 👎.