Skip to content

Per-request JSON-RPC log capture via the haskell-logging entry list#4151

Merged
automergerpr-permission-manager[bot] merged 9 commits into
masterfrom
per-request-rpc
Jun 4, 2026
Merged

Per-request JSON-RPC log capture via the haskell-logging entry list#4151
automergerpr-permission-manager[bot] merged 9 commits into
masterfrom
per-request-rpc

Conversation

@ehildenb
Copy link
Copy Markdown
Member

@ehildenb ehildenb commented Jun 3, 2026

Adds an opt-in haskell-logging field to every JSON-RPC request: the request names which log entries to capture, and the matching entries from both engines are returned in-band on the response's haskell-log-entries field. This lets a client (pyk/KEVM) collect structured backend logs per request — with exact per-request attribution — without parsing a shared stderr/file stream or correlating across concurrent server processes. The set of entries to capture lives in the request (and thus downstream), so it can evolve without a backend release; the backend stays a generic "capture these entry types for this request, return them in-band" mechanism with no hardcoded policy. Capture is purely additive — the existing stderr/file logger is unaffected — and is independent of the server's -l/--log-file configuration. Nothing/[] capture nothing.

Changes:

  • New wire fields on all five request/result types: haskell-logging :: Maybe [Text] (the entry/context names to capture) on requests, haskell-log-entries :: Maybe [Value] on results. Defaults to absent, so existing clients are unaffected.
  • A thread-safe per-request Collector (Kore.JsonRpc.Types.LogCapture) that both engines write into; the proxy drains it into haskell-log-entries at response time.
  • The flat name list spans both engines and is passed to each; each matches only the names it recognises (kore entry-type names via the log registry; booster context names via clContextName, with tag-only matching for id-carrying contexts such as CtxRewrite), so a name unknown to both is silently skipped — keeping client/backend version skew safe.
  • Kore-side capture is composed outside koreLogFilters, so it does not depend on the server's -l/--log-entries configuration; unrecognised names are skipped at runtime rather than failing the request.
  • BoosterAdaptor.entryToJsonValue is factored out of renderJson so captured entries are byte-for-byte equivalent (modulo whitespace) to the --log-format json file output.
  • Requires claude to run hlint on every commit.

Validation:

  • cabal build all, fourmolu, and hlint pass at every commit.
  • New RPC integration test (test-haskell-log-capture) sends a name list and asserts subset selection; runs under kore-rpc-booster in CI.
  • Verified end-to-end against the IMP and a-to-f definitions: a kore-only name list returns only kore-engine entries, a booster-only list returns only booster entries, narrowing to ["Proxy"] returns only proxy-context entries, an unknown name is skipped without error, and an empty/absent field captures nothing.

ehildenb and others added 2 commits June 3, 2026 16:52
… flag and haskell-log-entries response field

Adds the new wire fields to all five JSON-RPC request/result types
(execute, simplify, implies, get-model, add-module) with no behavioural
change.  `haskell-logging :: Maybe Bool` on requests and
`haskell-log-entries :: Maybe [Value]` on results lay the groundwork
for the per-request log-bundle capture described in the downstream
spec; current results all set `Nothing` for the new field.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…Capture: add per-request log-capture infrastructure

Introduces a shared `Collector` (TVar-backed unbounded buffer of
serialized log entries) and two engine-specific capture LogActions:

  * Kore.Log.LogCapture: filters SomeEntry by registered TypeRep
    (DebugAttemptEquation, DebugApplyEquation, DebugTerm) and writes
    entries as JSON Values via the new entryToJsonValue helper extracted
    from renderJson.
  * Booster.Log.LogCapture: filters LogMessage by CLContext presence
    (CtxProxy, CtxDetail, CtxAbort, CtxSimplify), plus a teeLogger
    combinator for fan-out.

No wiring yet — these modules are stand-alone primitives the proxy
will compose against per-request.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@ehildenb
Copy link
Copy Markdown
Member Author

ehildenb commented Jun 3, 2026

Note that because this is adding logging contexts in various places in the RPC server, it may be better to review with git diff -b to aviod the whitespace changes.

ehildenb and others added 3 commits June 3, 2026 17:22
…ire per-request capture into both engine loggers

Adds two engine-side wiring components for the LogCapture
infrastructure:

  * `withBoosterCapture` runs a `LoggerMIO` action with the
    booster-side capture tee'd onto the existing logger, no-op when
    no collector is supplied.
  * `KoreCaptureRegistry` is a `TVar (Map ThreadId Collector)` owned
    by `KoreServer`; the static kore log action consults it via the
    new `registryLogAction`, fanning matching entries to the
    collector registered for the calling thread.  `withKoreCapture`
    brackets a request's IO action with the per-thread
    registration.

`Server.hs` constructs one registry at startup, appends
`registryLogAction registry` to `koreLogActions`, and threads the
registry through `mkKoreServer` into the new `KoreServer.captureRegistry`
field.  No request handler consults the registry yet — that wiring
comes in the per-endpoint plumbing commits.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…kell-logging capture

Threads the new flag through the proxy's main dispatch.  Before
case-dispatching, respondEither now wraps the inner action with a
single helper that:

  1. Creates a per-request Collector when the request opts in.
  2. Installs the booster-side tee via withBoosterCapture.
  3. Registers the collector against the calling thread in the
     shared KoreCaptureRegistry so kore log entries land in the
     same buffer.
  4. Drains the buffer and writes haskellLogEntries onto whichever
     result variant was produced.

ProxyConfig gains the (shared) KoreCaptureRegistry; Server.hs
wires it.  Result reconstruction uses RecordWildCards so the
@DuplicateRecordFields@-shared field name resolves unambiguously
through the API GADT.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…r-integration-tests: add round-trip test for haskell-logging capture

Adds an RPC integration test for the per-request `haskell-logging` flag:
sends an `execute` request with `haskell-logging: true` and asserts the
response carries a non-empty `haskell-log-entries` array, plus a control
request without the flag asserting the field is omitted. Reuses the small
`a-to-f` definition via a symlink. The capture happens in the proxy, so the
test runs only under kore-rpc-booster.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ehildenb ehildenb marked this pull request as ready for review June 3, 2026 20:49
@ehildenb ehildenb requested a review from jberthold June 3, 2026 20:49
@ehildenb ehildenb marked this pull request as draft June 3, 2026 21:27
…ster/tools/{Proxy,Server}, test-haskell-log-capture: carry per-request haskell-logging entry list

Replaces the fixed-bundle `haskellLogging :: Maybe Bool` flag with
`haskellLogging :: Maybe [Text]` on all five endpoints: the request now names
exactly which log entries to capture, so the default set lives downstream (pyk)
and can evolve without a backend release. `Nothing`/`Just []` capture nothing;
`Just names` captures exactly the named entries.

The flat name list spans both engines and is passed to each; each matches only
the names it recognises and ignores the rest, so a name unknown to both is
silently skipped (making pyk/backend version skew safe):

- kore: names are resolved to entry types via the log registry
  (`koreTypesFromNames`), skipping unknowns instead of erroring at module load;
  the per-request type set is registered per thread. The capture action is also
  composed outside `koreLogFilters`, so capture no longer depends on the
  server's `-l`/`--log-entries` configuration.
- booster: names match against a message's context stack by `clContextName`
  (new, in `ContextLog`) — the `Ctx`-stripped CamelCase constructor name, which
  is a tag-only match for the `UniqueId`-carrying contexts (`"Rewrite"` matches
  any `CtxRewrite _`).

The integration test now sends a name list and asserts subset selection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ehildenb ehildenb changed the title Per-request JSON-RPC log capture via haskell-logging flag Per-request JSON-RPC log capture via the haskell-logging entry list Jun 3, 2026
CI enforces HLint as well as Fourmolu; note that both must pass on every commit,
not just the final one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ehildenb ehildenb marked this pull request as ready for review June 3, 2026 23:56
Copy link
Copy Markdown
Collaborator

@jberthold jberthold left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idiom of "injecting" the captured logs after the fact is a bit confusing at first (assigning haskellLogentries = Nothing everywhere) but a good way to add this with few code changes.
I would still refactor it to use the existing LogLine type and existing rendering code paths where possible.

, valid :: Bool
, condition :: Maybe Condition
, logs :: Maybe [LogEntry]
, haskellLogEntries :: Maybe [Value]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be good to use the LogEntry type here, just like the context log.

data LogLine = LogLine
{ timestamp :: Maybe SystemTime
, context :: Seq CLContext
, message :: CLMessage
}
deriving stock (Show, Eq)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the LogLine type would start in this file, replacing all Value instances by LogLine (and importing it).
Other files would follow the type changes, and there will be some code shared with the existing context logging.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

…ster/tools/Proxy: capture entries as structured LogLine, not Value

Addresses review feedback: store captured entries as the structured `LogLine`
type (from `ContextLog`, the same type the context log uses) instead of opaque
`Value`. The `Collector` now buffers `LogLine`s and the `haskell-log-entries`
response field is `Maybe [LogLine]`.

- booster: builds a `LogLine` directly — its context stack is already
  `[CLContext]`, and the message is wrapped as `CLText`/`CLValue`.
- kore: reuses `entryToJsonValue` (so a captured entry stays identical to its
  on-disk JSON-log form) and parses it back into `LogLine`; kore's context
  vocabulary is the same `CLContext` the context log models. A parse failure
  falls back to a context-less line carrying the raw value, keeping capture
  total.

The emitted JSON is unchanged (`LogLine`'s ToJSON is the `{timestamp?, context,
message}` shape), so the wire format is identical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ehildenb
Copy link
Copy Markdown
Member Author

ehildenb commented Jun 4, 2026

Done — switched the Collector, the haskell-log-entries field, and both capture paths to the structured LogLine type. Booster builds it directly from its [CLContext] stack; kore reuses entryToJsonValue and parses into LogLine (its context vocabulary is the same CLContext), with a total fallback. The emitted JSON is unchanged since LogLine's ToJSON is the same {context, message} shape. (bc0e7fe)

`teeLogger` and `boosterCaptureLogger` are only used internally by
`withBoosterCapture`; drop them from the export list to keep the module's API
surface minimal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@automergerpr-permission-manager automergerpr-permission-manager Bot merged commit e3cfc9d into master Jun 4, 2026
6 checks passed
@automergerpr-permission-manager automergerpr-permission-manager Bot deleted the per-request-rpc branch June 4, 2026 16:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants