Repository navigation
Conversation
Add pact.logging, which initialises the Pact FFI once with a silent sink and registers the library and plugin log callbacks, re-emitting every event on the pact.ffi.<target> and pact.plugins loggers with the test run ID and mock server or plugin instance ID as record attributes. The forwarding level follows any explicit level on those loggers, and otherwise defaults to DEBUG. Initialisation is implicit because the FFI logger can be applied once per process and users should not have to know that. Pact and Verifier will call ensure_initialised() on construction, so the first use of the library wires everything up. A logger applied earlier through one of the pact_ffi.log_to_* functions is kept, with a warning on the pact logger, and the callbacks attach to it instead. ensure_initialised() also registers an atexit hook which sets the callback level to OFF and deregisters both callbacks. A callback fired from a library thread during interpreter finalisation cannot acquire the GIL and aborts the process, so the hook cuts the callbacks off before finalisation starts. The test suite no longer initialises FFI logging to stderr itself, as initialisation is now the library's responsibility. tests/interaction/test_http_interaction.py::test_pact_server_verbose counted every record caplog captured, expecting only the `pact.pact` mismatch record. The mock server's own events now reach caplog's root handler through the `pact.ffi.*` loggers, so the assertion filters to records from `pact.pact`. Signed-off-by: JP-Ellis <josh@jpellis.me> Assisted-by: Claude Code:claude-opus-5-5
Pact and Verifier call pact.logging.ensure_initialised on construction, so a test suite gets library and plugin logs through logging with no set-up of its own. ensure_initialised is a no-op past the first call in the process, while the FFI callbacks are process-wide. A test elsewhere in the suite that registers and deregisters its own callback leaves pact.logging's callbacks unregistered. TestCallbacks re-registers them after ensure_initialised so it does not depend on suite order. Signed-off-by: JP-Ellis <josh@jpellis.me> Assisted-by: Claude Code:claude-opus-5-5
Rewrite the logging page around the automatic forwarding: the loggers and record attributes, controlling verbosity through logger levels, correlating records with a test run ID, and configuring the FFI's own sinks as the escape hatch. The examples no longer initialise FFI logging to stderr, as the library now does its own initialisation. Fix cross-reference syntax in src/pact/logging.py: `mkdocs build --strict` reported them unresolved. mkdocstrings' scoped_crossrefs resolves an unqualified reference relative to the referencing module, but a `pact.`-prefixed target from a module below the package root gets that prefix prepended again (e.g. `pact.Verifier` from `pact.logging` resolves to `pact.pact.Verifier`), so cross-module references need the path relative to the `pact` package instead of the full `pact.`-prefixed path. Within `pact/logging.py` itself, the module's own `import logging` shadows a relative `logging.x` self-reference, so those use the bare member name instead. Signed-off-by: JP-Ellis <josh@jpellis.me> Assisted-by: Claude Code:claude-opus-5-5
Observed DEBUG output from the consumer suite (tests/test_pact.py), provider verification (tests/test_verifier.py) and an HTTP example (examples/http/requests_and_fastapi) with --log-cli-level=DEBUG. In all three, DEBUG volume was dominated by "FFI function invoked" tracing on every call and per-request matching dumps (headers, body, matching rules) that add nothing once a test passes; running the fake plugin driver additionally showed a burst of hyper/rustls/reqwest transport chatter from the plugin driver's metrics beacon. INFO keeps the messages that matter (catalogue updates, plugin lifecycle, warnings and errors) without that noise, so DEFAULT_LEVEL is now logging.INFO. Signed-off-by: JP-Ellis <josh@jpellis.me> Assisted-by: Claude Code:claude-opus-5
The unit tests in tests/test_logging.py drive the CFFI callback objects directly, so none of them proves that the Pact library reaches the Python callback from its own threads. Add a consumer test which starts a mock server, makes one request through it and asserts that records arrived on a pact.ffi.* logger with a mock server ID as log_id. The assertions stop at "some records, at least one with a log_id"; the count and wording of the library's events vary between Pact FFI releases and would make the test brittle. Signed-off-by: JP-Ellis <josh@jpellis.me> Assisted-by: Claude Code:claude-opus-5
pact.logging needs `register_log_callback` and `set_log_callback_level`, added in Pact FFI 0.5.9. Under the previous `~=0.5.0` specifier, an older pact-python-ffi raised AttributeError on the first `Pact` or `Verifier`. 0.5.12 makes deregistering a log callback safe while another thread is still running it. pact.logging deregisters both callbacks at interpreter exit, when a mock server or plugin thread may still be emitting. Signed-off-by: JP-Ellis <josh@jpellis.me> Assisted-by: Claude Code:claude-opus-5-5
Test suites used to call `pact_ffi.log_to_stderr`, usually from a session-scoped conftest fixture, to see the Pact library's output. Pact Python now applies the library's logger on the first `Pact` or `Verifier`, and that logger can be applied once per process. An existing call made after that point failed with "Logger already initialized". Importing `pact` now replaces `init`, `init_with_log_level`, `log_to_stderr`, `log_to_stdout` and `log_to_file` on `pact_ffi` with functions which emit a `UserWarning` and return without applying anything. A suite which still calls one of them keeps running, and the warning asks for the call to be removed in favour of `logging` configuration. A call made before `pact` is imported still applies the library's own sink, which Pact Python keeps and warns about on the `pact` logger, as before. `log_to_buffer` is left in place, since pact.logging applies its silent sink through it. The consumer and provider guides no longer recommend `log_to_stderr`, and the logging page explains the change and how to get the library's output on stderr through `logging`. Signed-off-by: JP-Ellis <josh@jpellis.me> Assisted-by: Claude Code:claude-opus-5-5
Pact FFI reports `verify:<provider name>` as the log ID of events emitted while verifying a provider, alongside the mock server ID while a mock server handles a request. The verifier also generates its own test run ID for the verification, which most of its records carry in place of the ID set with `set_test_run_id`. Describe both on the logging page, in the pact.logging module docstring and in `set_test_run_id`. Records from the library's own threads are grouped by `log_id`, not `test_run_id`. Cover the verification path end to end, asserting that a record arrives with `log_id == "verify:provider"`. Signed-off-by: JP-Ellis <josh@jpellis.me> Assisted-by: Claude Code:claude-opus-5-5
Events from the Pact library were emitted on `pact.ffi.<target>` with the Rust module path as given, such as `pact.ffi.pact_matching::matchers`. The `logging` hierarchy only splits names on `.`, so that logger's parent was `pact.ffi` and a level set on `pact.ffi.pact_matching` did not reach it. Replace `::` with `.` in the target, so records from `pact_matching::matchers` go to `pact.ffi.pact_matching.matchers`, and one crate or module can be filtered on its own. Signed-off-by: JP-Ellis <josh@jpellis.me> Assisted-by: Claude Code:claude-opus-5-5
A fake plugin under PACT_PLUGIN_DIR writes a line to stderr and exits without completing the handshake. The line arrives on `pact.plugins` with the plugin instance ID, and the driver's own records about loading the plugin carry the test run ID set on the calling thread. The plugin's stderr line has no test run ID: it is not tied to any request. The test run ID reaches a plugin only as part of a request, so only the log entries a plugin sends back for that request carry it. The logging page and `set_test_run_id` claimed every plugin record carried the ID; they now say which ones do. Signed-off-by: JP-Ellis <josh@jpellis.me> Assisted-by: Claude Code:claude-opus-5-5
JP-Ellis
added this pull request to stack #1733
October 16, 2026 00:37
This was referenced Oct 16, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
📝 Summary
Pact Python now logs like a native Python library. Events from the Pact FFI and from plugins are re-emitted through the standard
loggingmodule, sopact_ffi.log_to_stderris no longer needed.pact.ffi.<module>. The Rust::separators become., sopact_matching::matcherslogs onpact.ffi.pact_matching.matchers. Each record carrieslog_idandtest_run_id.pact.plugins, withplugin_instance_id,test_run_idandplugin_target.PactorVerifier. The forwarding level is the most verbose level set onpact.ffiorpact.plugins, orINFOwhen neither is set.log_idis the mock server ID while the server handles a request, orverify:<provider>during verification.This is the first of three stacked PRs. #1731 adds a pytest plugin and a
unittestmixin which set the test run ID per test. #1732 exposes the plugin log entries buffered by the FFI.🚨 Breaking Changes
pact_ffi.log_to_stderr,log_to_stdout,log_to_file,initandinit_with_log_levelnow emit aUserWarningand do nothing oncepactis imported. Remove those calls and configurelogginginstead, as described indocs/logging.md.🔥 Motivation
The FFI logged to its own sinks, outside the Python interpreter. Its output bypassed pytest's log capture and handlers configured in user code. Correlating it with a test meant reading a separate stream.
Pact FFI 0.5.9 added a log callback, and 0.5.10 added
log_idfor verification. Forwarding through the callback lets every standardloggingtool work on Pact output.pact-python-ffiis pinned to 0.5.12, which waits for an in-flight callback before deregistration returns. Without that, deregistering at interpreter exit could free a callback while a mock server thread was still running it. This PR stays in draft until 0.5.12 is released.🔨 Test Plan
log_to_*stand-ins.log_id == "verify:<provider>"), and a fake plugin underPACT_PLUGIN_DIR. The plugin test is skipped on Windows, as the fake plugin is a shell script.🔗 Related issues/PRs