refactor(operation-trace): isolate subsystem, fix install identity, merge migrations

- Move the operation-traceability service and its HTTP routes into a standalone
  pkg/operation_trace package; the Core controller package no longer depends on it
  (routes stay auto-discovered by the controller package scan).
- Gate the hot path on one module-level boolean exposed as ap.operation_trace_active:
  while no Workspace has tracing enabled the route wrapper performs zero trace work
  and zero database round trips. The gate opens when a Workspace enables tracing and
  is primed once at startup for already-enabled Workspaces.
- Record the installed extension identity for GitHub / marketplace / local / upload
  installs (owner/repo, author/name, filename) so the trace no longer shows a
  nameless "a plugin was installed" event and the card names the extension.
- Merge migrations 0032 + 0033 into a single 0032_operation_traceability revision
  carrying the full table shape and an index set that matches the ORM.

Tests: unit 5165 passed / integration 358 passed; ruff check + format clean.
This commit is contained in:
TyperBody
2026-09-27 00:11:57 +08:00
parent becb6c78b4
commit 9964f284e1
19 changed files with 216 additions and 131 deletions
+11 -5
View File
@@ -363,6 +363,13 @@ class RouterGroup(abc.ABC):
database, so a disabled Workspace pays nothing at steady state.
"""
# Cheap global gate: a single attribute read on the application object.
# While no Workspace has enabled traceability this is ``False`` and the
# isolated subsystem stays entirely off the hot request path, with no
# import, service lookup, JSON parse or database round trip.
if not getattr(self.ap, 'operation_trace_active', False):
return
service = getattr(self.ap, 'workspace_settings_service', None)
if service is None:
return
@@ -394,16 +401,15 @@ class RouterGroup(abc.ABC):
# touching each handler. The body is read defensively: an audit path
# must never fail (or block) because a request carries no JSON.
try:
# Imported lazily so the base controller never participates in an
# import cycle with the service layer at module load time.
from ..service import settings as settings_service
# Identity resolution lives in the isolated traceability service
# and is reached through the handle, so the Core controller never
# imports that subsystem.
request_body: dict[str, typing.Any] | None = None
if str(quart.request.method or '').upper() not in ('GET', 'HEAD', 'OPTIONS'):
raw_body = await quart.request.get_json(silent=True)
if isinstance(raw_body, dict):
request_body = raw_body
identity = settings_service.resolve_resource_identity(kwargs, request_body)
identity = service.resolve_resource_identity(kwargs, request_body)
if identity:
resource_id = identity
except Exception: # pragma: no cover - identity resolution is best effort