dc8c62a7a9
## Context ## AI generated graph goal example <img width="2078" height="914" alt="image" src="https://github.com/user-attachments/assets/14c760df-5ac8-4526-a5e0-40b57344be90" /> <img width="2158" height="1850" alt="image" src="https://github.com/user-attachments/assets/2324779e-9ab8-47fc-a014-78269a78823e" /> The workspace migration builder and runner already log phase timings via `logger.perfTime`, but those logs are only useful for local debugging: they're gated behind the `performance` log level and never leave the process. This PR routes the same timings into the existing `MetricsService` (OpenTelemetry) so migration performance can be tracked over time and regressions detected. ## What's recorded All metrics are histograms recorded via `MetricsService.recordHistogram`, which is a synchronous in-memory aggregation - export happens on the background OTel readers (Prometheus scrape or periodic OTLP push), so nothing is added to the migration hot path. When `METER_DRIVER` is unset, records are no-ops. | Metric | Where | Attributes | |---|---|---| | `workspace-migration/build-duration-ms` | `WorkspaceMigrationValidateBuildAndRunService` | `status`: `success`, `fail` (validation errors), `error` (builder threw) | | `workspace-migration/build-entity-duration-ms` | `WorkspaceEntityMigrationBuilderService.validateAndBuild` | `metadataName`, `status` | | `workspace-migration/build-entity-phase-duration-ms` | entity builder | `metadataName`, `phase`: `matrix-computation`, `deletion-validation`, `creation-validation`, `update-validation` | | `workspace-migration/run-duration-ms` | `WorkspaceMigrationRunnerService.run` | `status`: `success`, `fail` - recorded on every exit path, including pre-transaction failures (DDL locked, cache retrieval, application not found) | | `workspace-migration/run-phase-duration-ms` | runner | `phase`: `initial-cache-retrieval`, `action-execution`, `commit`, `cache-invalidation`; `status` - on failure the elapsed transaction time is recorded as `action-execution` with `status: 'fail'` (the action/commit split is unknowable mid-failure) | | `workspace-migration/action-duration-ms` | base action handler wrapper | `actionType`, `metadataName`, `step`, `status` - failed steps are recorded via try/finally | | `workspace-migration/action-count` | validate-build-and-run service | - | Phase notes: phases are disjoint (commit is subtracted from transaction time) but not exhaustive - connection setup, the application-map fetch, rollback, and after-commit side effects sit outside them, so the stack approximates rather than equals the run total. The `cache-invalidation` phase is recorded only at the run's post-commit call site, not inside the public `invalidateCache`, so standalone callers (upgrade backfill commands, `FlatCacheInvalidateCommand`) don't pollute the series. ## Implementation notes - Durations are measured with local `performance.now()` instead of reusing `perfTime`/`perfTimeEnd`, so metrics are recorded regardless of the `performance` log level and are immune to key collisions in the logger's shared timer map under concurrent migrations. Existing perf logs are untouched. - `WorkspaceMigrationRunnerService.run` is now a thin wrapper around the previous body (`executeRun`) so the total run duration and its success/fail status are recorded in one place for every throw path. - No `workspaceId` in attributes to keep cardinality bounded. - Two new bucket-boundary constants follow the existing `AI_LATENCY_MS_BUCKET_BOUNDARIES` pattern: durations 5ms-120s, action counts 1-5000. - `MetricsModule` imported into `WorkspaceMigrationModule`, `WorkspaceMigrationRunnerModule`, `WorkspaceMigrationBuilderModule`, and the action-handlers module. ## Test - `nx typecheck twenty-server` passes - oxlint + oxfmt clean on changed files - `workspace.service.spec.ts` passes