|
| 1 | +# Request Observability Architecture |
| 2 | + |
| 3 | +This document is normative for Studio's request observability surface over Prisma Streams `evlog` and `otel-traces` streams. |
| 4 | + |
| 5 | +The feature is a stream-detail drilldown, not a standalone Studio view. It lets users expand an observability event or span row, open a request detail sheet, and inspect the correlated event, trace timeline, trace waterfall, errors, service calls, and partial-result warnings returned by Prisma Streams. |
| 6 | + |
| 7 | +## Scope |
| 8 | + |
| 9 | +This architecture governs: |
| 10 | + |
| 11 | +- detection of observability-capable stream profiles |
| 12 | +- URL-backed request lookup state |
| 13 | +- loading request correlation data from Prisma Streams |
| 14 | +- rendering the request detail sheet from a single correlation response |
| 15 | +- demo seeding for local request-observability validation |
| 16 | + |
| 17 | +## Canonical Components |
| 18 | + |
| 19 | +- [`ui/hooks/use-stream-observe-request.ts`](../ui/hooks/use-stream-observe-request.ts) |
| 20 | +- [`ui/studio/views/stream/StreamObserveSheet.tsx`](../ui/studio/views/stream/StreamObserveSheet.tsx) |
| 21 | +- [`ui/studio/views/stream/StreamObserveTimelineSection.tsx`](../ui/studio/views/stream/StreamObserveTimelineSection.tsx) |
| 22 | +- [`ui/studio/views/stream/StreamObserveTraceSection.tsx`](../ui/studio/views/stream/StreamObserveTraceSection.tsx) |
| 23 | +- [`ui/studio/views/stream/StreamObserveEventSection.tsx`](../ui/studio/views/stream/StreamObserveEventSection.tsx) |
| 24 | +- [`ui/studio/views/stream/StreamView.tsx`](../ui/studio/views/stream/StreamView.tsx) |
| 25 | +- [`demo/ppg-dev/seed-streams.ts`](../demo/ppg-dev/seed-streams.ts) |
| 26 | +- [`demo/ppg-dev/seed-streams-scale.ts`](../demo/ppg-dev/seed-streams-scale.ts) |
| 27 | + |
| 28 | +## Non-Negotiable Rules |
| 29 | + |
| 30 | +- Request observability MUST only appear for streams whose resolved profile is `evlog` or `otel-traces`. |
| 31 | +- Stream profile detection and request-pair descriptors MUST come from Streams metadata normalized by `useStreams` and `useStreamDetails`; feature code MUST NOT infer observability support from stream names. |
| 32 | +- The active lookup MUST be URL-backed through `streamObserve` and `useNavigation`; components MUST NOT write or parse `window.location.hash` directly. |
| 33 | +- `streamObserve` values MUST serialize as `req:<requestId>`, `trace:<traceId>`, or `span:<spanId>`. |
| 34 | +- Expanded event rows MAY expose the request-detail action only when the decoded event body has a usable request ID, trace ID, or span ID for the active profile. |
| 35 | +- Correlation loading MUST go through `useStreamObserveRequest`; view components MUST NOT call `/v1/observe/request` directly. |
| 36 | +- The request sheet MUST treat the Streams response as authoritative and surface `coverage.warnings` when present. |
| 37 | +- Missing counterpart streams MUST be explained in the sheet instead of rendering an empty trace or event section as complete. |
| 38 | +- The UI MUST use ShadCN primitives for the sheet, badges, buttons, skeletons, and section selector. The waterfall and timeline are custom request-observability composites and are documented in [`non-standard-ui.md`](non-standard-ui.md). |
| 39 | + |
| 40 | +## API Contract |
| 41 | + |
| 42 | +Studio expects the configured Streams base URL to expose: |
| 43 | + |
| 44 | +- `POST {streamsUrl}/v1/observe/request` |
| 45 | + |
| 46 | +The hook sends: |
| 47 | + |
| 48 | +```json |
| 49 | +{ |
| 50 | + "streams": { |
| 51 | + "events": "app-events", |
| 52 | + "traces": "app-traces" |
| 53 | + }, |
| 54 | + "lookup": { |
| 55 | + "requestId": "req_123" |
| 56 | + }, |
| 57 | + "include": { |
| 58 | + "events": true, |
| 59 | + "trace": true, |
| 60 | + "timeline": true |
| 61 | + }, |
| 62 | + "limits": { |
| 63 | + "events": 50, |
| 64 | + "spans": 2000 |
| 65 | + } |
| 66 | +} |
| 67 | +``` |
| 68 | + |
| 69 | +`lookup` contains exactly one of `requestId`, `traceId`, or `spanId`. If one counterpart stream is unavailable, Studio omits that stream and sets the matching include flag to `false`. |
| 70 | + |
| 71 | +The response is normalized into: |
| 72 | + |
| 73 | +- `lookup`: resolved request, trace, and span IDs |
| 74 | +- `summary`: title, method/path, service, environment, duration, status, level, and error summary fields |
| 75 | +- `evlog`: the primary event plus match count |
| 76 | +- `trace`: deduplicated spans, tree, critical path, errors, service map, and partial-state metadata |
| 77 | +- `timeline`: merged event/span timeline items |
| 78 | +- `coverage`: searched sides and warnings |
| 79 | + |
| 80 | +The sheet renders three sections from that one response: |
| 81 | + |
| 82 | +- `Timeline`: merged event, span-start, span-event, and exception items |
| 83 | +- `Trace`: waterfall rows, span details, errors, and service calls |
| 84 | +- `Event`: primary evlog event, root-cause fields, and raw JSON |
| 85 | + |
| 86 | +## Pairing Model |
| 87 | + |
| 88 | +When the active stream is `evlog`, Studio uses that stream as the event stream. Its trace counterpart MUST come from `details.observability.request.tracesStream`. |
| 89 | +When the active stream is `otel-traces`, Studio uses that stream as the trace stream. Its event counterpart MUST come from `details.observability.request.eventsStream`. |
| 90 | + |
| 91 | +If the descriptor is absent, Studio may still open the sheet for the active stream side and MUST explain the missing event or trace side. Studio MUST NOT choose the first stream with the opposite profile. |
| 92 | + |
| 93 | +## Demo Contract |
| 94 | + |
| 95 | +`pnpm demo:ppg` seeds two local profiled streams: |
| 96 | + |
| 97 | +- `app-events` with the `evlog` profile |
| 98 | +- `app-traces` with the `otel-traces` profile |
| 99 | + |
| 100 | +The seed data MUST include successful requests, failed requests with root-cause fields, slow requests, event-only requests, trace-only requests, and at least one deeper multi-service trace that exercises nested service calls, repeated network spans, and downstream worker/service spans. The demo also starts a ticker that appends fresh correlated requests so `Tail` mode and request-detail refresh can be exercised locally. |
| 101 | + |
| 102 | +The demo MUST create both streams with `Content-Type: application/json` before profile installation, and the installed profiles MUST declare their request-observability counterparts. |
| 103 | + |
| 104 | +`pnpm demo:ppg:seed-scale -- --streams-url <url>` appends deterministic scale data to the same two streams. It MUST use the shared seed builder so local performance checks exercise the same profile shape as `pnpm demo:ppg`. |
| 105 | + |
| 106 | +## Forbidden Patterns |
| 107 | + |
| 108 | +- matching observability streams by hard-coded stream names in the UI |
| 109 | +- storing request-detail data in component-local state outside React Query |
| 110 | +- adding request-observability methods to the database adapter |
| 111 | +- hiding coverage warnings or partial trace state |
| 112 | +- inventing request IDs from arbitrary payload text |
| 113 | +- adding a standalone request-observability route before the stream row workflow needs one |
| 114 | + |
| 115 | +## Testing Requirements |
| 116 | + |
| 117 | +Request observability changes MUST include tests for: |
| 118 | + |
| 119 | +- lookup param serialization and parsing |
| 120 | +- event-row lookup extraction for both `evlog` and `otel-traces` |
| 121 | +- descriptor-based counterpart stream resolution without first-profile fallback |
| 122 | +- `useStreamObserveRequest` request body, disabled state, failure state, and response normalization |
| 123 | +- sheet loading, warning, timeline, trace, event, missing-stream, and close behavior |
| 124 | +- stream-row affordance visibility and URL-backed sheet opening |
| 125 | +- demo seed shape, profiled stream creation, and scale-seed batch generation |
0 commit comments