Skip to content

feat(ktor): persist a per-session trace journal - #65

Open
Ziedelth wants to merge 2 commits into
masterfrom
feat/ktor-session-tracking
Open

Ziedelth wants to merge 2 commits into
masterfrom
feat/ktor-session-tracking

Conversation

@Ziedelth

Copy link
Copy Markdown
Contributor

Summary

Adds a persistent, zero-configuration session journal to the Ktor module.

  • X-Session-Id, X-Request-Id, X-Parent-Request-Id: resolved at the edge, generated when absent, echoed on the response, and exposed through CORS.
  • createHttpClient() forwards the identifiers, so a session follows a service-to-service call.
  • One append-only JSONL journal per session and per service ($SHIKKANIME_LOGS_DIR/sessions/<service>/<sessionId>.jsonl), written by a single background thread with a bounded queue, retention and crash-safe resume.
  • GET /v1/session-traces/{sessionId} and GET /v1/session-traces, registered only when SHIKKANIME_SESSION_TOKEN is set and protected by Authorization: Bearer.
  • Controller failures are captured through ErrorAttribute, so the IN event carries the exception type and message.

Design

  • The request thread never performs file I/O: SessionStore.append offers to a bounded queue and drops on saturation (droppedEvents); the writer runs at minimum priority.
  • The ktor module keeps no dependency on exposed or cache. A database-backed SessionStore is supplied by the consumer through SessionConfig.store; the framework leaves its lifecycle to the caller.
  • Session and request identifiers become file names; only [A-Za-z0-9_-]{8,64} is accepted, and a malformed header is replaced rather than used as a path.
  • /health, /swagger, /openapi, /v1/session-traces and OPTIONS are excluded from tracking.
  • Tracking is on by default; SessionConfig(enabled = false) opts out.

Verification

  • ./gradlew test all modules: green.
  • :ktor:test: 78 tests.
  • Two-service example (gateway :8080 -> catalog :8081), scripts/verify.sh:
    • success, 500, slow and chained calls;
    • gateway and catalog journals share the session, and OUT.requestId == IN.requestId;
    • restart persistence: 7 events across a restart;
    • read API: 401 missing/wrong token, 200 with token, 404 unknown, 400 invalid, list OK;
    • two 5,000-request / 50-connection load runs, 0 failures, 10,000 events written, eventCount=10000, truncated=true at limit=1000.
  • Load, 2-vCPU shared host (client and server co-located):
    • tracking: 1,688.6 rps, p50 15.21 ms, p99 211.79 ms;
    • baseline: 2,094.7 rps, p50 12.51 ms, p99 146.41 ms.
    • The delta is CPU, not blocking: the request path only enqueues; the writer is a single background thread. p50 overhead is ~2.7 ms at 50 concurrent calls on this host.
  • Qodana: 0 problems.

Release note

The framework version is bumped to 0.0.27-SNAPSHOT. The snapshot was not published: the available GitHub token only has read:packages (write:packages returns 401). The example uses a Gradle composite build against ../framework; a maintainer token with write:packages is needed to publish before consumers can resolve the snapshot.

Add session tracking to every application using configureDefaultModules():

- resolve X-Session-Id, X-Request-Id and X-Parent-Request-Id, generate
  missing identifiers, echo them on the response and expose them through
  CORS so a browser client can keep its session;
- forward the identifiers from createHttpClient(), including the parent
  request id, so one session can be followed across services;
- write one append-only JSONL journal per session and per service through a
  single background writer with a bounded queue, retention and crash-safe
  resume; append() never performs file I/O on the request thread and drops
  on saturation instead of slowing a request;
- expose a bearer-protected read API under /v1/session-traces only when a
  token is configured;
- record controller failures through ErrorAttribute so the IN event carries
  the exception type and message.

The ktor module keeps no dependency on exposed or cache: a database-backed
SessionStore is supplied by the consumer through SessionConfig.store.

Verified with ./gradlew test (all modules), the :ktor:test suite (78 tests),
and a two-service example covering success, 500, slow and chained calls,
restart persistence, and two 5000-request load runs with 10000 events
written and no failures.

See guidelines/SESSION_TRACKING.md.
Silence the unused-symbol findings on public framework APIs with
@Suppress("unused") and the reason, so a library consumed by downstream
projects is not reported as dead code. Fix the real findings at the same
time: unused imports, unresolved KDoc links, the deprecated readRemaining()
call, and the redundant suspend/runBlocking pair in the argument-resolver
test.

No behavior change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant