|
1 | 1 | # Cycles Protocol v0.1.25 — Server Implementation Audit |
2 | 2 |
|
3 | | -**Date:** 2026-04-16 (v0.1.25.13 — hydration cap + enum wire annotations on the sorted `GET /v1/reservations` path; `SORTED_HYDRATE_CAP=2000` guard on the in-memory sort hydration with WARN-on-cap, matches admin plane's v0.1.25.24 pattern; `@JsonValue`/`@JsonCreator fromWire` on `ReservationSortBy` + `SortDirection` to mirror admin's `SortSpec`/`SortDirection` contract), |
| 3 | +**Date:** 2026-04-18 (v0.1.25.14 — trace_id (W3C Trace Context) cross-surface correlation per cycles-protocol revision 2026-04-18; new `TraceContextFilter` extracts `traceparent` or `X-Cycles-Trace-Id` from inbound requests or generates a fresh 128-bit id, echoes `X-Cycles-Trace-Id` on every response, populates `trace_id` on `ErrorResponse` / `Event` / `WebhookDelivery` / `AuditLogEntry`), |
| 4 | +2026-04-16 (v0.1.25.13 — hydration cap + enum wire annotations on the sorted `GET /v1/reservations` path; `SORTED_HYDRATE_CAP=2000` guard on the in-memory sort hydration with WARN-on-cap, matches admin plane's v0.1.25.24 pattern; `@JsonValue`/`@JsonCreator fromWire` on `ReservationSortBy` + `SortDirection` to mirror admin's `SortSpec`/`SortDirection` contract), |
4 | 5 | 2026-04-16 (v0.1.25.12 — `sort_by` + `sort_dir` on `GET /v1/reservations` per cycles-protocol spec revision 2026-04-16; 7-value sort enum, opaque cursor binds `(sort_by, sort_dir, filters)` tuple, legacy SCAN-cursor path preserved when both params omitted), |
5 | 6 | 2026-04-14 (automated performance regression detection — nightly trend + release gate, no version bump), |
6 | 7 | 2026-04-14 (nightly soak test — long-duration stability coverage, no version bump), |
|
19 | 20 |
|
20 | 21 | --- |
21 | 22 |
|
| 23 | +### 2026-04-18 — v0.1.25.14: trace_id cross-surface correlation (W3C Trace Context) |
| 24 | + |
| 25 | +Implements the `CORRELATION AND TRACING` normative section added to `cycles-protocol-v0.yaml` in spec revision 2026-04-18 (commit `8d65959`). Introduces a third correlation identifier — `trace_id` — that is W3C Trace Context-compatible (OpenTelemetry-native) and links every HTTP request to its `ErrorResponse`, audit-log entry, emitted events, and outbound webhook deliveries under one logical-operation grain. |
| 26 | + |
| 27 | +**Inbound header extraction** (new `TraceContextFilter`, `@Order(0)`, runs before `RequestIdFilter`): |
| 28 | + |
| 29 | +1. `traceparent` header — parsed as W3C Trace Context version 00 (`^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$`). Must be non-all-zero trace-id and span-id per W3C §3.2.2.3. |
| 30 | +2. `X-Cycles-Trace-Id` header — parsed as flat 32 lowercase hex; must be non-all-zero. Used when `traceparent` is absent or malformed. |
| 31 | +3. Server generates — `SecureRandom` 16 bytes → 32 lowercase hex; re-rolled if all-zero. |
| 32 | + |
| 33 | +Malformed headers are silently ignored (spec: MUST NOT reject). When both valid headers are present and disagree, `traceparent` wins per OpenTelemetry interop precedence. |
| 34 | + |
| 35 | +Inbound trace-flags preservation on outbound webhooks is deferred to the sibling `cycles-server-events` PR — that PR needs to add a `trace_flags` field to the `WebhookDelivery` schema (or accept the v0 default of `01`/sampled on every outbound delivery). Not a wire-compliance gap for this PR: only the outbound webhook `traceparent` is affected, and it's emitted by the events service, not this server. |
| 36 | + |
| 37 | +**Outbound propagation:** |
| 38 | + |
| 39 | +- Every response (2xx/4xx/5xx) echoes `X-Cycles-Trace-Id` header. |
| 40 | +- `ErrorResponse.trace_id` populated across all five exception-handler paths (`CyclesProtocolException`, `MethodArgumentNotValidException`, `ConstraintViolationException`, `HttpMessageNotReadableException`, generic 500). |
| 41 | +- `Event.trace_id` populated for every event emitted via `EventEmitterService.emit(...)` and `emitBalanceEvents(...)` — RESERVATION_DENIED, RESERVATION_COMMIT_OVERAGE, BUDGET_EXHAUSTED, BUDGET_OVER_LIMIT_ENTERED, BUDGET_DEBT_INCURRED, and RESERVATION_EXPIRED. |
| 42 | +- `WebhookDelivery.trace_id` copied from `Event.trace_id` in `EventEmitterRepository.createDelivery` so the events service can lift it into outbound headers without re-parsing the event body. |
| 43 | +- `AuditLogEntry.trace_id` populated on admin-driven releases (`ReservationController.release`). |
| 44 | +- `ReservationExpiryService` mints a fresh trace_id per sweep batch so all reservation.expired events in one sweep correlate to each other. Per spec, `request_id` remains null on sweeper-generated events (no originating HTTP request). |
| 45 | + |
| 46 | +**SLF4J MDC:** filter sets `traceId` key on entry and removes it in `finally` so every log line produced during the request carries the trace_id automatically. Existing `requestId` MDC key behavior unchanged. |
| 47 | + |
| 48 | +**Contract impact:** purely additive. `trace_id` is an OPTIONAL property on `ErrorResponse` (schema preserves `additionalProperties: false` via a declared property). Response header is additive; clients that don't read it are unaffected. Inbound `traceparent` / `X-Cycles-Trace-Id` headers are additive; clients that don't send them are unaffected. |
| 49 | + |
| 50 | +**Admin spec v0.1.25.28 alignment (WebhookDelivery):** also adds the two companion fields defined by governance-admin spec revision 2026-04-18 on the shared `WebhookDelivery` schema: |
| 51 | + |
| 52 | +- `trace_flags` (`^[0-9a-f]{2}$`) — W3C Trace Context trace-flags byte. Preserves the inbound sampling decision when the originating request carried a valid `traceparent`; defaults to `01` (sampled) when the trace was derived from `X-Cycles-Trace-Id` or server-generated. |
| 53 | +- `traceparent_inbound_valid` (boolean) — whether the originating HTTP request presented a valid inbound W3C `traceparent`. Consumed by the `cycles-server-events` sidecar to decide whether to preserve `trace_flags` on the outbound delivery or default to `01`. |
| 54 | + |
| 55 | +Both fields are threaded from the `TraceContextFilter` through an internal `TraceContext` record (`cycles-protocol-service-data/.../data/util/TraceContext.java`), which also collapses the per-request correlation trio (`trace_id` + `trace_flags` + `traceparent_inbound_valid`) into a single positional parameter on `EventEmitterService.emit(...)` and `emitBalanceEvents(...)` — addressing the param-sprawl concern flagged in the simplify review without adding 2 extra positional args. |
| 56 | + |
| 57 | +`Event.java` carries the two companion fields as `@JsonIgnore` transient properties so they travel with the `Event` object through the async emit path without bleeding into the `Event` wire contract (the spec only declares these on `WebhookDelivery`). |
| 58 | + |
| 59 | +**Out of scope (sibling PRs):** |
| 60 | + |
| 61 | +- `cycles-server-events` (separate repo): outbound webhook `X-Cycles-Trace-Id` and `traceparent` headers. Reads `trace_id`, `trace_flags`, and `traceparent_inbound_valid` directly off the `WebhookDelivery` row from Redis. This PR prepares the row fully so the events-service PR is a straight read-and-forward. |
| 62 | +- `cycles-server-admin`: admin-plane `AuditLogEntry.trace_id` surfacing and new `listEvents`/`listAuditLogs` `trace_id`/`request_id` filter query parameters. |
| 63 | + |
| 64 | +**Files changed:** |
| 65 | + |
| 66 | +- **NEW** `cycles-protocol-service-api/src/main/java/io/runcycles/protocol/api/filter/TraceContextFilter.java` — the filter, regex-based traceparent parsing. |
| 67 | +- **NEW** `cycles-protocol-service-data/src/main/java/io/runcycles/protocol/data/util/TraceIdGenerator.java` — shared pure-function helper (`SecureRandom` → 32-hex with all-zero re-roll) used by the filter fallback path and by `ReservationExpiryService`. |
| 68 | +- **NEW** `cycles-protocol-service-data/src/main/java/io/runcycles/protocol/data/util/TraceContext.java` — record bundling `trace_id` + `trace_flags` + `traceparent_inbound_valid`; threaded through `EventEmitterService.emit(...)` as a single positional param instead of three parallel Strings/Booleans. |
| 69 | +- `cycles-protocol-service-model/src/main/java/io/runcycles/protocol/model/ErrorResponse.java` — `trace_id` field. |
| 70 | +- `cycles-protocol-service-model/src/main/java/io/runcycles/protocol/model/event/Event.java` — `trace_id` field. |
| 71 | +- `cycles-protocol-service-model/src/main/java/io/runcycles/protocol/model/webhook/WebhookDelivery.java` — `trace_id` + `trace_flags` + `traceparent_inbound_valid` fields (admin spec v0.1.25.28). |
| 72 | +- `cycles-protocol-service-model/src/main/java/io/runcycles/protocol/model/audit/AuditLogEntry.java` — `trace_id` field. |
| 73 | +- `cycles-protocol-service-api/src/main/java/io/runcycles/protocol/api/controller/BaseController.java` — new `resolveRequestId` + `resolveTraceId` helpers. |
| 74 | +- `cycles-protocol-service-api/src/main/java/io/runcycles/protocol/api/controller/{ReservationController,DecisionController,EventController}.java` — pass `resolveRequestId(httpRequest)` and `resolveTraceId(httpRequest)` into every event-emission call site. |
| 75 | +- `cycles-protocol-service-api/src/main/java/io/runcycles/protocol/api/exception/GlobalExceptionHandler.java` — populate `trace_id` on every `ErrorResponse.builder()` across all five `@ExceptionHandler` paths. |
| 76 | +- `cycles-protocol-service-data/src/main/java/io/runcycles/protocol/data/service/EventEmitterService.java` — `traceId` parameter appended to `emit(...)` and the full `emitBalanceEvents(...)` signature; three prior `emitBalanceEvents(...)` overloads retained as delegating wrappers for source compatibility. |
| 77 | +- `cycles-protocol-service-data/src/main/java/io/runcycles/protocol/data/service/ReservationExpiryService.java` — batch-scope `TraceIdGenerator.generate()` per sweep, threaded into `emitExpiredEvent`. |
| 78 | +- `cycles-protocol-service-data/src/main/java/io/runcycles/protocol/data/repository/EventEmitterRepository.java` — `createDelivery` copies `event.getTraceId()` onto the `WebhookDelivery`. |
| 79 | + |
| 80 | +**Tests:** |
| 81 | + |
| 82 | +- **NEW** `TraceContextFilterTest` (13 cases) — traceparent valid/malformed/all-zero, X-Cycles-Trace-Id valid/malformed/all-zero/uppercase, both-present disagreement, fallthrough generation, response header echo, request attribute set, `currentTraceId(null)` handling, non-v00 version rejection, trace-flags=00 round-trip. |
| 83 | +- `GlobalExceptionHandlerTest` — `trace_id` assertions across all five handler paths; null-trace when filter didn't run. |
| 84 | + |
| 85 | +--- |
| 86 | + |
22 | 87 | ### 2026-04-16 — v0.1.25.13: hydration cap + enum wire annotations on the sorted list path |
23 | 88 |
|
24 | 89 | Closes two follow-up gaps surfaced by the three-step review of v0.1.25.12 against the admin-plane implementation of the same feature in `cycles-server-admin` v0.1.25.24: |
|
0 commit comments