v2.0.0 — Breaking changes. This plan removes fields from the
RouteDataentity and changes the source of metrics (aggregates fromRouteDataRecord). The 1.x branch remains stable; normalization is delivered in 2.0.0. See V2_MIGRATION.md for breaking changes and migration guide.
- routes_data: only information that defines the route (identity) + usage and review metadata.
- routes_data_records: one record per access to the route, with all metrics for that request.
- Be able to analyze by route: seasonality, 500s by parameters, etc., always querying over
records.
| Field | Current use |
|---|---|
| env, name | Identity (lookup by route+env) |
| httpMethod | Identity |
| params | Identity (JSON) |
| totalQueries | Aggregated metric / "worst case" |
| requestTime | Aggregated metric |
| queryTime | Aggregated metric |
| memoryUsage | Aggregated metric |
| accessCount | Access counter |
| statusCodes | Aggregated metric (counts per HTTP code) |
| createdAt | Metadata |
| updatedAt | Metadata |
| lastAccessedAt | Metadata |
| reviewed, reviewedAt, reviewedBy, queriesImproved, timeImproved | Review |
Currently the lookup key is (name, env); params is not part of the key, so the same route with different params is merged into a single row.
| Field | Current use |
|---|---|
| route_data_id | FK to RouteData |
| accessed_at | When |
| status_code | HTTP status |
| response_time | Time for that request |
Missing per record: total_queries, query_time, memory_usage. Without these, "worst case" or metric-based seasonality cannot be analyzed without relying on aggregates stored in RouteData.
Keep in routes_data:
| Field | Type / Notes |
|---|---|
| id | PK |
| env | string |
| name | string |
| http_method | string |
| params | JSON (nullable) — key to allow splitting by params later |
| created_at | datetime_immutable |
| last_accessed_at | datetime_immutable (last access; updated when inserting a record or via command) |
| reviewed | bool |
| reviewed_at | datetime_immutable nullable |
| reviewed_by | string nullable |
| queries_improved | bool nullable (optional) |
| time_improved | bool nullable (optional) |
Remove from RouteData (become derived from records only):
- totalQueries, requestTime, queryTime, memoryUsage
- accessCount
- statusCodes
- updatedAt (or keep only if the row is updated for review/lastAccessedAt)
The idea: one RouteData row = one "logical route" identified by (env, name, httpMethod, params). If you later split by params, the lookup key would be (env, name, httpMethod, params) and you could have multiple rows for the same name with different params.
Content of each record:
| Field | Type / Notes |
|---|---|
| id | PK |
| route_data_id | FK → routes_data |
| accessed_at | datetime_immutable |
| status_code | int nullable |
| response_time | float nullable (time for that request) |
| total_queries | int nullable (add) |
| query_time | float nullable (add) |
| memory_usage | int nullable (add, optional) |
With this, everything that is "metric of a request" lives in records. "Worst" values and counts are obtained via queries or an aggregation command (e.g. the current RebuildAggregates, extended).
Sync schema with entities: after modifying entities (adding/removing columns), run:
php bin/console nowo:performance:sync-schemaTo drop columns that no longer exist on the entity, use --drop-obsolete. See COMMANDS.md.
- Add to entity and migration:
totalQueries(int, nullable)queryTime(float, nullable)memoryUsage(int, nullable), if you want memory per request
- In
PerformanceMetricsService, when creating eachRouteDataRecord, also set:totalQueries,queryTime,memoryUsagewith the values for that request.
This way no information is lost and you can analyze per record.
- Decide lookup key:
- Option A: keep
(name, env)and do not use params in the key (params informational only). - Option B: key
(env, name, httpMethod, params)to allow splitting by params later.
- Option A: keep
- Remove from RouteData (and from the DB via migration):
- totalQueries, requestTime, queryTime, memoryUsage, accessCount, statusCodes, updatedAt (if removed).
- Keep in RouteData: env, name, httpMethod, params, createdAt, lastAccessedAt, reviewed, reviewedAt, reviewedBy (and optional queriesImproved, timeImproved).
- Repository:
- If using params in the key: new method
findByRouteEnvAndParams(string $name, string $env, ?array $params)(comparing normalized params, e.g. sorted JSON). - The service that currently uses
findByRouteAndEnvwould switch to this lookup (with optional or normalized params).
- If using params in the key: new method
- Source of truth: always
RouteDataRecord. - For listings and rankings (worst time, worst query count, etc.):
- Option 1: SQL/DQL queries that aggregate over
routes_data_records(MAX(response_time), MAX(total_queries), COUNT(*), GROUP BY route_data_id). - Option 2: aggregate cache table (one row per route_data_id with max_request_time, max_total_queries, access_count, status_counts) updated when inserting a record or via a command like
nowo:performance:rebuild-aggregates.
- Option 1: SQL/DQL queries that aggregate over
- RebuildAggregatesCommand: no longer writes statusCodes/accessCount to RouteData if those fields are removed; it could instead fill the aggregate cache table or rely entirely on real-time queries.
- Route detail (by RouteData id):
- List
RouteDataRecordfor thatroute_data_idwith filters:- Date range (
accessed_at) → seasonality. status_code = 500→ view only 500 errors.- Optional: group by params (if in the future params is also stored on the record or derived from RouteData).
- Date range (
- List
- Seasonality:
- Query records by
route_data_idand date range; aggregate by day/week/month (COUNT, AVG(response_time), MAX(response_time), COUNT when status_code=500).
- Query records by
- "Returns 500 for certain params":
- If you later split by params (multiple RouteData for the same name with different params), each has its own records; filter by
status_code = 500per RouteData. - If params is not split yet, 500s for that route are seen by filtering records by
route_data_idandstatus_code = 500(and optionally by params if stored on record).
- If you later split by params (multiple RouteData for the same name with different params), each has its own records; filter by
| Step | Action |
|---|---|
| 1 | Add to RouteDataRecord: totalQueries, queryTime, memoryUsage. Migration + fill in PerformanceMetricsService. |
| 2 | Decide whether RouteData key includes params (findByRouteEnvAndParams) and update repository + service. |
| 3 | Create migration that removes from RouteData: totalQueries, requestTime, queryTime, memoryUsage, accessCount, statusCodes (and updatedAt if desired). |
| 4 | Replace uses of those fields in controllers, commands and tests: read from records or an aggregate layer (query or cache). |
| 5 | Implement "route detail" view/API: list of records with filters (dates, status_code) and aggregations for seasonality. |
| 6 | (Optional) Aggregate table or materialized views if per-environment listings are slow. |
- routes_data: identity (env, name, httpMethod, params) + createdAt, lastAccessedAt, reviewed, reviewedAt, reviewedBy. No metrics.
- routes_data_records: per access: accessed_at, status_code, response_time, total_queries, query_time, memory_usage. All analyzable data goes here.
- Analysis by route: always over records (filter by route_data_id, dates, status_code; aggregate by time for seasonality).
- Splitting by params later: key (env, name, httpMethod, params) in RouteData; each combination has its own records.