Skip to content

feat: structured JSON logging via VOUCH_LOG_FORMAT=json#114

Merged
plind-junior merged 2 commits into
vouchdev:testfrom
Khaostica:fix/97-vouch-log-format-json
Jun 2, 2026
Merged

feat: structured JSON logging via VOUCH_LOG_FORMAT=json#114
plind-junior merged 2 commits into
vouchdev:testfrom
Khaostica:fix/97-vouch-log-format-json

Conversation

@Khaostica

@Khaostica Khaostica commented May 26, 2026

Copy link
Copy Markdown
Contributor

What changed

Implements VOUCH_LOG_FORMAT=json - already documented in ROADMAP.md and adapters/generic-mcp/README.md, but never wired up. New module src/vouch/logging_config.py exposes a JsonFormatter and an idempotent configure_logging(); the CLI (vouch.cli:cli), MCP server (vouch.server:run_stdio), and JSONL server (vouch.jsonl_server:run_jsonl) entry points each call it once at startup.

When VOUCH_LOG_FORMAT=json, vouch attaches a JSON handler to the vouch logger namespace with propagate=False. Each log record is emitted as one JSON object per line with level, logger, event, and any structured extras the caller attached via the stdlib extra= argument (notably actor and object_ids, to mirror the audit-log shape).

Any other value of VOUCH_LOG_FORMAT - including unset, empty, or text - is a no-op: vouch's loggers keep stdlib default behaviour, so callers who don't opt in see no change in routing or output.

Closes #97

Why

Running vouch as a service makes its logs hard to parse without a machine-readable format. VOUCH_LOG_FORMAT=json was already advertised in ROADMAP and the generic-MCP adapter docs as a supported knob, so operators reasonably expected it to work - implementing it closes that documentation/behaviour gap.

What might break

Nothing for existing users. With VOUCH_LOG_FORMAT unset (the default), configure_logging() does not touch the vouch logger's handlers, level, or propagation - vouch's loggers continue to behave exactly as they did before. Operators who explicitly opt into json will see output land on stderr as one JSON object per line; that is the documented contract.

No .vouch/ file shape changes. No kb.* method changes. No on-disk layout, bundle format, or audit-log event shape change.

VEP

Not needed. This does not change the object model, kb.* method surface, on-disk layout, bundle format, or audit-log event shape. It implements an environment-variable knob that is already documented in ROADMAP and adapters/generic-mcp/README.md.

Tests

  • make check passes locally (lint + mypy + pytest)
  • New / changed behaviour has a test
  • CHANGELOG.md updated under ## [Unreleased]

Detail:

  • make check verified in a fresh Linux env (WSL Ubuntu, Python 3.14.4, pip install -e '.[dev]'): ruff check src tests clean, mypy src clean ("Success: no issues found in 30 source files"), pytest 95 passed / 6 skipped / 0 failed.
  • New test module tests/test_logging.py (9 tests) — covers JsonFormatter field emission, extra= merging, event override, exception capture, env-var-driven configuration, idempotency, the text/JSON round-trip, and end-to-end one-object-per-line output via a real stdlib logger.
  • Smoke-tested manually: with VOUCH_LOG_FORMAT=json the vouch logger emits {"actor": "...", "event": "...", "level": "...", "logger": "...", "object_ids": [...]} to stderr; without it, plain text logging is unchanged.

Summary by CodeRabbit

  • New Features

    • Structured JSON logging via VOUCH_LOG_FORMAT=json — emits one-line JSON logs with fields like level, logger, event and any extras; text format remains default.
    • CLI, stdio/JSONL server entrypoints initialize logging at startup so the selected log format applies to commands and services.
  • Tests

    • Added comprehensive tests for JSON formatter, extras, exceptions, idempotence, and switching back to text.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: dc674efc-5243-43b9-93f3-7d850a1fd88d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds configurable structured JSON logging via VOUCH_LOG_FORMAT=json: a new logging_config module (JsonFormatter, selection, configure_logging) is idempotently wired into CLI, JSONL, and MCP server startups, documented in CHANGELOG, and covered by tests validating output and handler behavior.

Changes

JSON Logging Configuration and Integration

Layer / File(s) Summary
Core logging configuration module
src/vouch/logging_config.py
Implements JsonFormatter to serialize log records into single-line JSON objects with level, logger, event, merged extra fields, and exc when present. _selected_format() reads VOUCH_LOG_FORMAT (trim/ci). configure_logging() idempotently installs/removes a managed stderr handler and toggles propagate.
Integration into startup entry points
src/vouch/cli.py, src/vouch/jsonl_server.py, src/vouch/server.py
Each entry point imports and calls configure_logging() at process start (root cli() group, start of run_jsonl(), and before mcp.run()) so logging format is configured before application logic runs.
Comprehensive test suite
tests/test_logging.py
Autouse fixture snapshots/restores logger state. Tests cover required JSON fields, merging of extra, event override (including falsy values), exception inclusion, configure_logging() no-op/default mode, JSON-mode handler installation and idempotence, switching back to text, and end-to-end one-object-per-line emission.
Changelog documentation
CHANGELOG.md
Documents the new structured JSON logging feature under Unreleased, notes VOUCH_LOG_FORMAT=json behavior and that the default text format remains when unset.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I nibble logs in tidy rows,
Keys and values neatly posed,
CLI and servers hum in sync,
One JSON line per hop and wink,
VOUCH_LOG_FORMAT makes parsing close.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title clearly and concisely describes the main feature addition: structured JSON logging via the VOUCH_LOG_FORMAT=json environment variable.
Linked Issues check ✅ Passed All acceptance criteria from #97 are met: JSON logging emits required fields, default behavior preserved, and no functional changes beyond logging format.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the JSON logging feature; no unrelated modifications to APIs, bundle formats, or other systems.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/test_logging.py (1)

66-69: ⚡ Quick win

Add a regression test for falsy event override values.

Current coverage validates only truthy override values; a falsy-case test will guard the override semantics.

Proposed test addition
 def test_json_formatter_event_override_wins_over_message():
     line = _emit({"event": "kb.approve"})
     payload = json.loads(line)
     assert payload["event"] == "kb.approve"
 
+def test_json_formatter_event_override_allows_empty_string():
+    line = _emit({"event": ""})
+    payload = json.loads(line)
+    assert payload["event"] == ""
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_logging.py` around lines 66 - 69, Add a regression test that
verifies falsy "event" override values win over the message fallback: create a
test (e.g., test_json_formatter_event_override_allows_falsy_values) that calls
_emit({"event": ""}) and asserts payload["event"] == "" (and optionally repeat
for other falsy values like 0 and False), ensuring the json formatter uses the
provided event value even when it's falsy rather than falling back to the
message.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/vouch/logging_config.py`:
- Line 59: The current event override uses a falsy-coalescing pattern that
treats explicit falsy values as absent; change the "event" assignment in the
logging_config's formatter/dict creation so it checks key presence instead of
truthiness (i.e., if "event" is a key in record.__dict__ use
record.__dict__["event"] even if falsy, otherwise use record.getMessage());
update the code path where the dict entry "event": ... is built (the line using
record.__dict__.get("event") or record.getMessage()) to perform an explicit
key-existence test and return the message only when the key is not present.

---

Nitpick comments:
In `@tests/test_logging.py`:
- Around line 66-69: Add a regression test that verifies falsy "event" override
values win over the message fallback: create a test (e.g.,
test_json_formatter_event_override_allows_falsy_values) that calls
_emit({"event": ""}) and asserts payload["event"] == "" (and optionally repeat
for other falsy values like 0 and False), ensuring the json formatter uses the
provided event value even when it's falsy rather than falling back to the
message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 02ece305-70b2-4a8b-8b18-569b9e6cafd0

📥 Commits

Reviewing files that changed from the base of the PR and between 3beb821 and ceffeba.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • src/vouch/cli.py
  • src/vouch/jsonl_server.py
  • src/vouch/logging_config.py
  • src/vouch/server.py
  • tests/test_logging.py

Comment thread src/vouch/logging_config.py Outdated
@Khaostica Khaostica force-pushed the fix/97-vouch-log-format-json branch from ceffeba to 92b597b Compare May 26, 2026 21:54

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
tests/test_logging.py (1)

95-132: ⚡ Quick win

Add explicit no-op tests for VOUCH_LOG_FORMAT="" and "text"

Nice coverage overall, but the contract includes both empty-string and explicit text as no-op modes; adding those two cases would lock in the acceptance criteria and prevent regressions in format selection behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_logging.py` around lines 95 - 132, Add two explicit no-op tests
for the empty-string and explicit "text" VOUCH_LOG_FORMAT values: call
configure_logging() after monkeypatch.setenv("VOUCH_LOG_FORMAT", "") and after
monkeypatch.setenv("VOUCH_LOG_FORMAT", "text"), assert the returned selected
value is "text", that the logger retrieved with
logging.getLogger(VOUCH_LOGGER_NAME) has no handlers with attribute
_vouch_managed, and that logger.propagate is True; use the same conventions as
the existing tests (referencing configure_logging, VOUCH_LOG_FORMAT,
VOUCH_LOGGER_NAME, and JsonFormatter) so these behaviors are locked into the
test suite.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/test_logging.py`:
- Around line 95-132: Add two explicit no-op tests for the empty-string and
explicit "text" VOUCH_LOG_FORMAT values: call configure_logging() after
monkeypatch.setenv("VOUCH_LOG_FORMAT", "") and after
monkeypatch.setenv("VOUCH_LOG_FORMAT", "text"), assert the returned selected
value is "text", that the logger retrieved with
logging.getLogger(VOUCH_LOGGER_NAME) has no handlers with attribute
_vouch_managed, and that logger.propagate is True; use the same conventions as
the existing tests (referencing configure_logging, VOUCH_LOG_FORMAT,
VOUCH_LOGGER_NAME, and JsonFormatter) so these behaviors are locked into the
test suite.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 809a8284-bc71-4577-8bfa-48ff1b39a4d3

📥 Commits

Reviewing files that changed from the base of the PR and between ceffeba and 92b597b.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • src/vouch/cli.py
  • src/vouch/jsonl_server.py
  • src/vouch/logging_config.py
  • src/vouch/server.py
  • tests/test_logging.py
✅ Files skipped from review due to trivial changes (1)
  • src/vouch/jsonl_server.py

@Khaostica Khaostica force-pushed the fix/97-vouch-log-format-json branch from 92b597b to ddc51a4 Compare May 26, 2026 22:01
@Tet-9

Tet-9 commented May 27, 2026

Copy link
Copy Markdown
Contributor

@Khaostica , what's the meaning of this??!!

@Tet-9

Tet-9 commented May 27, 2026

Copy link
Copy Markdown
Contributor

Close it up immediately 😂

@Khaostica

Khaostica commented May 27, 2026

Copy link
Copy Markdown
Contributor Author

@Tet-9 looks like we were working on this in parallel — you opened #113 about an hour before I pushed this. Happy to defer to whoever the maintainer picks, but leaving #114 open since it emits event/actor/object_ids per the acceptance criteria on #97 and scopes the handler to the vouch logger namespace (so library logs aren't reformatted), which #113 doesn't currently do.

@Khaostica

Copy link
Copy Markdown
Contributor Author

@plind-junior Could you approve the workflow run?

@Tet-9

Tet-9 commented May 27, 2026

Copy link
Copy Markdown
Contributor

@Tet-9 looks like we were working on this in parallel — you opened #113 about an hour before I pushed this. Happy to defer to whoever the maintainer picks, but leaving #114 open since it emits event/actor/object_ids per the acceptance criteria on #97 and scopes the handler to the vouch logger namespace (so library logs aren't reformatted), which #113 doesn't currently do.

Lollllll
Man, you're so hilarious 😂...... The pr already spoke for itself, nothing to prove that otherwise, my pr stays above your pr, why is yours below mine??
And, there's an hour gap, really sweet of ya to reply back when the exact time couldn't be used to prove against ya, I'd at least vouch for you for that, cuz, it's commendable to use such a tactic 👏

@Tet-9

Tet-9 commented May 27, 2026

Copy link
Copy Markdown
Contributor

@Tet-9 looks like we were working on this in parallel — you opened #113 about an hour before I pushed this. Happy to defer to whoever the maintainer picks, but leaving #114 open since it emits event/actor/object_ids per the acceptance criteria on #97 and scopes the handler to the vouch logger namespace (so library logs aren't reformatted), which #113 doesn't currently do.

Although you're right 👍, we should leave it up to who the maintainer picks; survival of the fittest.
But, you're definitely below me by an hour..... That already is enough for proof.
Besides, why you left it for up to an hour before you push and commit on it?
Isn't that somewhat absurd 🤔, since clearly you know this is a competition, so, I'd actually like to redact the statement that we both worked on it in parallel, since there's no such proof to that, so sorry about that though 😞

@Khaostica

Copy link
Copy Markdown
Contributor Author

@Tet-9 this is an open source repo and multiple PRs on the same issue are welcome. That's the nature of open source contribution. And the maintainer's pick comes down to whichever diff matches the acceptance criteria on #97 — timing doesn't factor in.

@plind-junior plind-junior added the enhancement New feature or request label May 28, 2026
@plind-junior

Copy link
Copy Markdown
Collaborator

PR should target test branch

@Khaostica Khaostica changed the base branch from main to test May 28, 2026 13:04
@Khaostica Khaostica force-pushed the fix/97-vouch-log-format-json branch from ddc51a4 to ef6485b Compare May 28, 2026 18:46
…chdev#97)

Adds a `vouch.logging_config` module with a `JsonFormatter` and an
idempotent `configure_logging()` that the CLI, MCP server, and JSONL
server entry points call at startup.

When `VOUCH_LOG_FORMAT=json`, the `vouch` logger emits one JSON object
per line with `level`, `logger`, `event`, and any structured extras
(`actor`, `object_ids`, ...) passed via the stdlib `extra=` argument.
Any other value (including unset) is a no-op - vouch's loggers keep
stdlib default behaviour, so callers who don't opt in see no change.

Scoped to the `vouch` logger namespace with propagate=False in JSON
mode, so this never reformats logs emitted by libraries vouch happens
to depend on.
@Khaostica Khaostica force-pushed the fix/97-vouch-log-format-json branch from ef6485b to 7c8bd79 Compare May 29, 2026 13:52
@Khaostica

Copy link
Copy Markdown
Contributor Author

@plind-junior Could you approve the workflow?

@plind-junior

Copy link
Copy Markdown
Collaborator

Really nice — the env-var contract was sitting in our docs unimplemented, and you closed it cleanly. Two small things I want to call out as genuinely well done: writing to stderr (so the stdio MCP / JSONL server's stdout stream stays uncontaminated) and propagate=False only on opt-in (so anyone who doesn't set VOUCH_LOG_FORMAT=json sees zero behaviour change). Both are easy to get wrong.

One tiny non-blocker — the _vouch_managed = True attribute set on the handler instance (with the type: ignore[attr-defined]) works, but a small class _VouchManagedHandler(StreamHandler): ... would do the same thing without the attribute-hack and read a touch cleaner. Up to you.

Small process note: this is pointed at test rather than main, just checking that's intentional.

Thanks for taking it on! 🙏

@Khaostica

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review! 🙏

On the process note: yes, targeting test is intentional. test is the integration branch and is currently ahead of main; feature PRs land there first and reach main via the release branches (e.g. the recent release/0.1.0). So this is aimed where it should be.

On the handler: good call — I've adopted the subclass. There's now a small _VouchManagedHandler(logging.StreamHandler) that configure_logging() identifies by type (isinstance) for install/reuse/remove, which drops the _vouch_managed attribute and its type: ignore[attr-defined]. Tests updated to match. ruff, mypy src, and the logging tests all pass.

Replace the _vouch_managed attribute (and its type: ignore[attr-defined])
with a _VouchManagedHandler(StreamHandler) subclass. configure_logging()
identifies its handler by isinstance for install/reuse/remove, which reads
cleaner and drops the attribute hack. Tests updated to match.
@plind-junior plind-junior merged commit 7fa2104 into vouchdev:test Jun 2, 2026
1 check passed
Tet-9 added a commit to Tet-9/vouch that referenced this pull request Jun 2, 2026
…LEVEL/FILE, and LoggingConfig return type

Combines the best of both PR vouchdev#113 and vouchdev#114 into a superior implementation:

From vouchdev#114 (Khaostica):
- Scoped vouch logger namespace instead of root logger
- _STDLIB_RECORD_FIELDS explicit whitelist for extra= merging
- event key with caller override via extra={event: ...}
- sort_keys=True, default=str in json.dumps
- propagate=False in json mode to prevent double-emit
- _selected_format() helper

Added on top:
- time field in JSON output (ISO 8601)
- VOUCH_LOG_LEVEL env var: sets vouch logger level (default WARNING)
- VOUCH_LOG_FILE env var: appends logs to file alongside stderr
- Sensitive field redaction: keys containing secret/token/password/key
  are replaced with ***REDACTED*** so credentials never reach log sinks
- LoggingConfig named tuple return type for testability and introspection
- force=True parameter for clean reconfiguration in tests
- CONTRIBUTING.md: documents all env vars, logging guidelines, and
  managed-handler pattern
- 11 tests covering all new behaviour

Fixes vouchdev#97
Tet-9 added a commit to Tet-9/vouch that referenced this pull request Jun 2, 2026
…LEVEL/FILE, and LoggingConfig return type

Combines the best of both PR vouchdev#113 and vouchdev#114 into a superior implementation:

From vouchdev#114 (Khaostica):
- Scoped vouch logger namespace instead of root logger
- _STDLIB_RECORD_FIELDS explicit whitelist for extra= merging
- event key with caller override via extra={event: ...}
- sort_keys=True, default=str in json.dumps
- propagate=False in json mode to prevent double-emit
- _selected_format() helper

Added on top:
- time field in JSON output (ISO 8601)
- VOUCH_LOG_LEVEL env var: sets vouch logger level (default WARNING)
- VOUCH_LOG_FILE env var: appends logs to file alongside stderr
- Sensitive field redaction: keys containing secret/token/password/key
  are replaced with ***REDACTED*** so credentials never reach log sinks
- LoggingConfig named tuple return type for testability and introspection
- force=True parameter for clean reconfiguration in tests
- CONTRIBUTING.md: documents all env vars, logging guidelines, and
  managed-handler pattern
- 11 tests covering all new behaviour

Fixes vouchdev#97
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: structured JSON logging via VOUCH_LOG_FORMAT=json

3 participants