# Data Model: Full Observability No Prisma schema changes — every entity here is in-process or exported to an external observability sink, never persisted to Postgres. ## Request Context Store `AsyncLocalStorage`, populated once per request in `request-context.plugin.ts`'s existing `onRequest` hook (the same hook that already builds `request.reqContext`), read by `logger.ts`'s Pino `mixin` function on every subsequent log call made anywhere during that request's handling. | Field | Type | Notes | |---|---|---| | `requestId` | `string` | Same value already assigned to `request.reqContext.requestId` | | `correlationId` | `string` | Same value already assigned to `request.reqContext.correlationId` | ## Access Log Line (shape, not a stored entity) Emitted once per completed request via the existing `logger` singleton from the new `onResponse` hook. | Field | Type | Notes | |---|---|---| | `method` | `string` | HTTP method | | `route` | `string` | Parameterized route pattern (`request.routeOptions.url`), not the raw URL | | `statusCode` | `number` | Response status | | `durationMs` | `number` | `reply.elapsedTime` | | `requestId` / `correlationId` | `string` | Via the mixin, same as every other line for this request | | `event` | `string` | Fixed value `"http_request_completed"` — lets log queries filter to access-log lines specifically | Log level: `info` for 2xx/3xx, `warn` for 4xx, `error` for 5xx — mirrors the existing error-handler's own level choices (`app.ts`) so severity is consistent across both sources of request-outcome logging. ## Metrics (Prometheus, via `prom-client`) All registered in `infrastructure/observability/metrics.ts` on the existing default registry (`metricsRegistry`, already exposed at `GET /metrics`), all prefixed `supporthub_` to match the existing histogram and default-metrics prefix. | Metric name | Type | Labels | Incremented/observed when | |---|---|---|---| | `supporthub_http_request_duration_seconds` | Histogram *(existing, now actually observed)* | `method`, `route`, `status_code` | Every completed HTTP request | | `supporthub_ai_session_outcomes_total` | Counter | `outcome` (`resolved` \| `escalated`) | An AI support session reaches a terminal `resolved`/`escalated` status | | `supporthub_ticket_resolutions_total` | Counter | `resolved_by` (`ai` \| `human`) | A ticket reaches `RESOLVED`, labeled from the ticket's `Resolution.resolvedBy` | | `supporthub_ticket_resolution_duration_seconds` | Histogram | — | A ticket reaches `RESOLVED` — observes `resolvedAt - ticket.createdAt` | | `supporthub_ticket_first_response_duration_seconds` | Histogram | — | The first `AGENT_MESSAGE` is posted on a ticket — observes `firstResponseAt - ticket.createdAt` | | `supporthub_sla_run_outcomes_total` | Counter | `outcome` (`met` \| `breached`) | An SLA run completes on time (`met`) or is flagged by the breach sweep (`breached`) | | `supporthub_escalations_total` | Counter | `reason` | An `ESCALATION_TRIGGERED` domain event fires (already published unconditionally today) | | `supporthub_problems_created_total` | Counter | `category_id` (or `uncategorized`) | A `Problem` row is created (at ticket-intake time) | | `supporthub_known_error_lookups_total` | Counter | `code` | A valid error code's known issues are looked up | | `supporthub_knowledge_retrieval_outcomes_total` | Counter | `matched` (`true` \| `false`) | The AI's `searchProductKnowledge` tool call returns zero vs. one-or-more results | | `supporthub_tool_invocations_total` | Counter | `tool`, `outcome` (`success` \| `failed`) | Every AI tool-call result, any tool | Deliberately **not** separate metrics (per spec.md's Assumptions): "recurring problems" and "most common errors" are read directly off `supporthub_problems_created_total` and `supporthub_known_error_lookups_total` respectively via a monitoring stack's own `topk`/`rate` query — no additional "top N" metric or logic is computed by this application. ## Traces / Spans (exported, not persisted) | Span | Parent | Attributes | Created in | |---|---|---|---| | `ticket.create` | (root) | `ticket.id`, `product.externalProductId` | `ticketing/tickets/service/tickets.service.ts` | | `ai.escalation` | `ticket.create` (if within the same request) or its own root (async paths) | `ticket.id`, `session.id` | `ai-support/sessions/service/session.service.ts`, around the escalation branch | | `orchestration.assignment` | `ai.escalation` (via the `TICKET_UPDATED`/`HUMAN_ESCALATION` subscriber) | `ticket.id`, `strategy` | `orchestration/orchestration` + `orchestration/assignments`, wrapping the existing `handleHumanEscalation` call | Span context propagation across the domain-event bus relies on the OpenTelemetry Context API's own async-local propagation — since `eventBus.publish(...)` is `await`ed synchronously within the same call chain (confirmed in `tickets.service.ts`/`escalation.service.ts`), no manual context-carrying payload field is needed.