Skip to content

Commit 20dfe45

Browse files
authored
Merge pull request #107 from runcycles/feat/trace-id-correlation-v0.1.25.14
feat(trace-id): W3C Trace Context cross-surface correlation (v0.1.25.14)
2 parents c5ffb08 + 847dc39 commit 20dfe45

27 files changed

Lines changed: 704 additions & 33 deletions

File tree

AUDIT.md

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Cycles Protocol v0.1.25 — Server Implementation Audit
22

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),
45
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),
56
2026-04-14 (automated performance regression detection — nightly trend + release gate, no version bump),
67
2026-04-14 (nightly soak test — long-duration stability coverage, no version bump),
@@ -19,6 +20,70 @@
1920

2021
---
2122

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+
2287
### 2026-04-16 — v0.1.25.13: hydration cap + enum wire annotations on the sorted list path
2388

2489
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:

BENCHMARKS.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ running them would only measure environmental noise. Skipped releases:
2929
unchanged and all existing benchmarks exercise the legacy path.
3030
Benchmarks for the sorted path are worth adding once real tenant
3131
populations exercise the O(N) full-SCAN; see OPERATIONS.md.
32+
- **v0.1.25.13** — hydration cap + enum wire annotations on the sorted
33+
list path. Write-path unchanged.
34+
- **v0.1.25.14** — trace_id (W3C Trace Context) correlation. The new
35+
`TraceContextFilter` adds two regex matches + one secure-random
36+
read per request (~microseconds, no Redis / no locking / no
37+
allocation beyond a 32-char String). Request-path code only;
38+
Lua / reserve / commit / release / extend hot paths untouched.
39+
Benchmarks deliberately skipped — they would only measure
40+
environmental noise. [benchmark-skip]
3241

3342
Last benchmarked release: **v0.1.25.7**.
3443

CHANGELOG.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,45 @@ changes to request/response bodies or Lua-script semantics would require a
1414
minor bump. "Internal signature changes" (e.g. Java method parameters) are
1515
called out but are not breaking to API clients.
1616

17+
## [0.1.25.14] — 2026-04-18
18+
19+
### Added
20+
21+
- W3C Trace Context correlation per `cycles-protocol-v0.yaml` revision
22+
2026-04-18. Every response now carries an `X-Cycles-Trace-Id`
23+
header. The server accepts a `traceparent` (W3C version 00) or
24+
`X-Cycles-Trace-Id` header on inbound requests and echoes back the
25+
same trace_id; when neither is present it generates a fresh 128-bit
26+
id (32 lowercase hex). Malformed headers are silently ignored; the
27+
server never rejects a request for a bad correlation header.
28+
- `trace_id` field on `ErrorResponse`, `Event`, `WebhookDelivery`,
29+
and `AuditLogEntry` bodies. Optional for wire back-compat; conformant
30+
servers populate it on every payload causally downstream of the
31+
request.
32+
- `trace_flags` (`^[0-9a-f]{2}$`) and `traceparent_inbound_valid`
33+
(boolean) on `WebhookDelivery` per governance-admin spec v0.1.25.28.
34+
These preserve the upstream W3C sampling decision so the events
35+
sidecar can reconstruct an outbound `traceparent` with the correct
36+
trace-flags byte instead of defaulting to `01`.
37+
- SLF4J MDC now carries `traceId` alongside `requestId` for every
38+
request — log aggregators can group by trace_id to see all lines
39+
produced during a single logical operation.
40+
- `ReservationExpiryService` mints a fresh trace_id per sweep batch
41+
so `reservation.expired` events emitted in the same sweep correlate
42+
to each other.
43+
44+
### Internal
45+
46+
- New `TraceContextFilter` (`@Order(0)`) runs before `RequestIdFilter`
47+
and sets the `cyclesTraceId` request attribute for downstream code.
48+
- `EventEmitterService.emit(...)` gains a final `String traceId`
49+
parameter. The full-arity `emitBalanceEvents(...)` signature
50+
likewise. Three prior overloads kept as delegating wrappers
51+
(`traceId = null`) for source compatibility with existing tests.
52+
- `BaseController` exposes protected `resolveRequestId` and
53+
`resolveTraceId` helpers that controllers use to thread the ids
54+
into event-emission and audit-log calls.
55+
1756
## [0.1.25.13] — 2026-04-16
1857

1958
### Fixed

OPERATIONS.md

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ section of [`README.md`](README.md) first.
1515
3. [SLO definitions](#slo-definitions)
1616
4. [Dashboards](#dashboards)
1717
5. [Incident playbook](#incident-playbook)
18-
6. [Configuration tuning](#configuration-tuning)
18+
6. [Correlation and tracing](#correlation-and-tracing)
19+
7. [Configuration tuning](#configuration-tuning)
1920

2021
---
2122

@@ -351,6 +352,55 @@ reservations. Either:
351352

352353
---
353354

355+
## Correlation and tracing
356+
357+
Every response carries two correlation identifiers (v0.1.25.14+):
358+
359+
| Header | Shape | Grain |
360+
|---|---|---|
361+
| `X-Request-Id` | UUIDv4 | One HTTP request |
362+
| `X-Cycles-Trace-Id` | 32 lowercase hex (W3C Trace Context) | One logical operation |
363+
364+
### Client-supplied trace_id
365+
366+
Clients can set the trace_id themselves by sending either:
367+
368+
- `traceparent: 00-{trace_id}-{span_id}-{trace_flags}` — standard W3C Trace Context header (OpenTelemetry-native). Takes precedence.
369+
- `X-Cycles-Trace-Id: {trace_id}` — flat 32-hex, for clients not using OpenTelemetry.
370+
371+
If both are present and disagree, `traceparent` wins. Malformed values are silently ignored (the server never 400s for a bad correlation header).
372+
373+
### Propagation
374+
375+
The trace_id is attached to:
376+
377+
- The response (`X-Cycles-Trace-Id` header).
378+
- `ErrorResponse` bodies (`trace_id` field).
379+
- Emitted events (`trace_id` field in the event body and on the `WebhookDelivery` row).
380+
- Audit-log entries for admin-driven releases (`trace_id` field in `AuditLogEntry`).
381+
382+
Sweeper-generated events (`reservation.expired`) get a fresh trace_id per sweep batch — they have no originating HTTP request.
383+
384+
### Log correlation
385+
386+
Every log line produced during a request carries both `requestId` and `traceId` MDC keys. Grep a trace_id across all log lines to see everything that happened in one logical operation:
387+
388+
```bash
389+
grep '4bf92f3577b34da6a3ce929d0e0e4736' server.log
390+
```
391+
392+
### Webhook correlation
393+
394+
Outbound webhook deliveries (from `cycles-server-events`) carry the same `trace_id` in the `X-Cycles-Trace-Id` and `traceparent` headers, plus the `trace_id` field in the event body. Subscribers can correlate their downstream processing back to the originating Cycles request. This server persists three correlation fields on the `WebhookDelivery` Redis row so the events sidecar can lift them straight into the outbound HTTP request:
395+
396+
- `trace_id` — the trace identifier.
397+
- `trace_flags` — the W3C trace-flags byte. When an inbound request carried a valid `traceparent`, this is the inbound byte (preserves sampling decision); otherwise `01`.
398+
- `traceparent_inbound_valid` — boolean. Tells the sidecar whether to preserve the above `trace_flags` (`true`) or default to `01` (`false`).
399+
400+
Wire-up of the outbound HTTP headers themselves happens in the `cycles-server-events` repo.
401+
402+
---
403+
354404
## Configuration tuning
355405

356406
All configurable via `application.properties` or environment variables.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ mvn clean install
117117
./build-all.sh
118118
```
119119

120-
The fat JAR is produced at `cycles-protocol-service-api/target/cycles-protocol-service-api-<version>.jar` (where `<version>` is the `revision` property in `cycles-protocol-service/pom.xml` — e.g. `0.1.25.13`).
120+
The fat JAR is produced at `cycles-protocol-service-api/target/cycles-protocol-service-api-<version>.jar` (where `<version>` is the `revision` property in `cycles-protocol-service/pom.xml` — e.g. `0.1.25.14`).
121121

122122
## Docker Deployment
123123

@@ -136,7 +136,7 @@ Pre-built images are published to GitHub Container Registry on each release:
136136

137137
```
138138
ghcr.io/runcycles/cycles-server:latest
139-
ghcr.io/runcycles/cycles-server:<version> # e.g. 0.1.25.13
139+
ghcr.io/runcycles/cycles-server:<version> # e.g. 0.1.25.14
140140
```
141141

142142
## Testing

cycles-protocol-service/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,13 +227,14 @@ For operations by resource ID (e.g. `GET /v1/reservations/{id}`), the server fir
227227

228228
| Order | Filter | Responsibility |
229229
|---|---|---|
230+
| 0 | `TraceContextFilter` | Extracts `traceparent` / `X-Cycles-Trace-Id` (or generates) → request attribute, MDC, `X-Cycles-Trace-Id` response header |
230231
| 1 | `RequestIdFilter` | Generates `X-Request-Id` UUID, stored as request attribute and response header |
231232
| 2 | `RateLimitHeaderFilter` | Adds `X-RateLimit-Remaining` / `X-RateLimit-Reset` headers (sentinel values in v0) |
232233
| 3 | `ApiKeyAuthenticationFilter` | Validates key, populates `SecurityContext`, sets `X-Cycles-Tenant` header |
233234

234235
Public paths (Swagger UI, actuator health, etc.) bypass authentication.
235236

236-
All responses include an `X-Request-Id` header. All timestamps are Unix milliseconds (int64).
237+
All responses include `X-Request-Id` and `X-Cycles-Trace-Id` headers. All timestamps are Unix milliseconds (int64). `trace_id` is W3C Trace Context-compatible (32 lowercase hex); clients may pre-supply it via `traceparent` (OpenTelemetry-native) or `X-Cycles-Trace-Id`. See `OPERATIONS.md` → "Correlation and tracing" for the full contract.
237238

238239
---
239240

@@ -688,6 +689,7 @@ cycles-protocol-service/
688689
│ ├── ApiKeyAuthentication.java # SecurityContext token
689690
│ └── SecurityConfig.java # Spring Security config
690691
├── filter/
692+
│ ├── TraceContextFilter.java # X-Cycles-Trace-Id / traceparent extraction
691693
│ ├── RequestIdFilter.java # X-Request-Id generation
692694
│ └── RateLimitHeaderFilter.java # rate limit headers (stub)
693695
└── exception/GlobalExceptionHandler.java

0 commit comments

Comments
 (0)