Logging integration: make the logger a library citizen, and use it - #384
Conversation
pcapkit configured logging at import: a single flat logger named 'pcapkit', with a StreamHandler on stderr attached and the level forced from PCAPKIT_DEVMODE. Importing the library therefore hijacked the consumer's logging, and undoing it meant reaching into logger.handlers. There was also no way to change verbosity at runtime, since the environment variable is read once. Now a NullHandler and no level at import, so verbosity is inherited from the application, with the old stderr handler kept for PCAPKIT_DEVMODE as the opt-in path. Alongside the existing logger object: get_logger() for per-module children, configure() to set level, handler, stream, format or propagation at runtime and per logger name, reset() to return to library-neutral, and ensure_output() for the verbose= path. Seventeen modules take getLogger(__name__), so a consumer can silence pcapkit.foundation.registry while keeping pcapkit.foundation.extraction. The library was also nearly silent where it mattered and noisy where it did not. 38 info calls become debug - 34 of them registry bookkeeping, 4 extractor configuration - leaving exactly one info in the package, the pcapkit-vendor CLI progress line. Four print calls that were clearly meant to be logging, each self-documenting with a logging-fstring-interpolation suppression, become logger.debug with lazy % args. And 31 new debug calls cover what was undiagnosable: extractor lifecycle, engine selection and fallback, reassembly and trace-flow entry points. Nothing was added to per-frame or per-field loops. Behaviour changes a consumer could notice, all documented in a compatibility note on the new docs page: no stderr handler at import (restore with configure(logging.INFO, stream=sys.stderr)); registry messages invisible even at INFO; handler is no longer logger.handlers[0]; and verbose=True logs at debug, so ensure_output() attaches a stderr handler only when nothing up the chain would receive the record - otherwise Extractor(verbose=True) would have gone silent. warnings.py keeps its behaviour: the double-emit and the global simplefilter mutation are filed as #363 and #364 and want their own change.
Converting the four verbose= handlers to logger.debug moved user-facing output onto the logger, which changed both its stream and its visibility: the CLI's -v frame chains went from stdout to stderr and only appeared if the consumer had configured a handler. tests/integration/test_cli_subprocess.py caught it. Those chains are a feature of the tool, not diagnostics, so they stay on print. That also removes the need for ensure_output() on the library path -- it existed only to stop verbose=True falling silent once the import-time handler was gone. The helper itself stays public and tested, since it is a reasonable thing for a consumer to call. Every genuine debug call added by this branch is untouched; only the four verbose handlers revert. Docs note and test updated to state the rule: verbose= is stdout, logging is diagnostics. Also converted register_sctp's logger.info to debug, which the branch had missed because SCTP landed on main after it was written -- the branch's own "no info on the registry path" test now passes. Full suite: 529 passed, 4 skipped, 303 subtests passed.
There was a problem hiding this comment.
🟡 Changes recommended
The current import-time reset() in pcapkit.utilities.logging can clobber an application's pre-existing pcapkit logger configuration, which undermines the stated “library-citizen” goal.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This PR refactors PyPCAPKit’s logging to behave like a well-mannered library (no import-time output configuration), introduces a runtime configuration API (get_logger, configure, reset, ensure_output), and updates key modules to log via per-module child loggers with rebalanced log levels and added DEBUG trail coverage. It also adds substantial automated test coverage and new documentation for the logging system.
Changes:
- Replace import-time logging configuration with a library-neutral default (NullHandler) plus a runtime configuration API for applications.
- Migrate modules to use per-module child loggers (
logger = get_logger(__name__)) and rebalance registry/extractor messages toward DEBUG. - Add comprehensive tests for import-time behavior, hierarchy, configuration/reset semantics, and extraction/verbose behavior; add new docs page and update utilities index + PEP checklist.
File summaries
| File | Description |
|---|---|
| tests/utilities/test_logging.py | Expands tests to cover import-time neutrality, hierarchy, configuration/reset/ensure_output, and extractor debug/verbose behavior. |
| pcapkit/vendor/main.py | Switches CLI module to a child logger under the pcapkit hierarchy. |
| pcapkit/utilities/warnings.py | Moves warnings module logging to a per-module child logger via get_logger(__name__). |
| pcapkit/utilities/logging.py | Implements logging hierarchy + runtime configuration API; updates import-time behavior and devmode bootstrap. |
| pcapkit/utilities/exceptions.py | Moves exceptions module logging to a per-module child logger via get_logger(__name__). |
| pcapkit/utilities/decorators.py | Moves decorators module logging to a per-module child logger via get_logger(__name__). |
| pcapkit/utilities/init.py | Re-exports the new logging configuration API from pcapkit.utilities. |
| pcapkit/protocols/transport/transport.py | Migrates transport protocol base to a per-module child logger. |
| pcapkit/foundation/traceflow/traceflow.py | Adds a per-module logger and new DEBUG messages around dumper/output selection and initialization. |
| pcapkit/foundation/traceflow/tcp.py | Adds a per-module logger and new DEBUG trail for TCP flow lifecycle and submission. |
| pcapkit/foundation/registry/protocols.py | Migrates registry logging to child logger and demotes “registered …” messages from INFO to DEBUG. |
| pcapkit/foundation/registry/foundation.py | Migrates registry logging to child logger and demotes bookkeeping messages to DEBUG. |
| pcapkit/foundation/reassembly/reassembly.py | Adds a per-module logger and DEBUG trail for reassembly lifecycle and counts. |
| pcapkit/foundation/extraction.py | Migrates extraction logging to child logger and adds a detailed DEBUG trail across the extraction lifecycle. |
| pcapkit/foundation/engines/scapy.py | Migrates engine logging to child logger and adds DEBUG engine activity messages. |
| pcapkit/foundation/engines/pyshark.py | Migrates engine logging to child logger and adds DEBUG messages for capability fallback and engine activity. |
| pcapkit/foundation/engines/pcapng.py | Migrates engine logging to child logger and adds DEBUG section-header visibility. |
| pcapkit/foundation/engines/pcap.py | Migrates engine logging to child logger and adds DEBUG visibility for PCAP global header properties. |
| pcapkit/foundation/engines/dpkt.py | Migrates engine logging to child logger and adds DEBUG engine activity messages. |
| pcapkit/dumpkit/common.py | Migrates dumpkit common logging to a per-module child logger. |
| docs/source/pep.rst | Marks the Logging Integration proposal largely done and documents the delivered scope. |
| docs/source/pcapkit/utilities/logging.rst | Adds dedicated logging documentation page including hierarchy, API, recipes, and compatibility notes. |
| docs/source/pcapkit/utilities/index.rst | Moves logging docs out of the index body into the utilities toctree as its own page. |
Review details
- Files reviewed: 22/23 changed files
- Comments generated: 8
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The new verbose and debug-trail tests read arp.pcap, which the generators build rather than the repository carrying, so the unit-test workflow - which deliberately runs without generated fixtures - failed on every Python version with FileNotFoundError. Same trap as the reassembly case in #376. Switched to in.pcap, one of the six committed captures, and corrected the expected frame count from 2 to 6. Both tests belong in the unit tier: they assert logging behaviour, not capture contents, so any real capture will do. Verified by running the CI selection with only the committed captures present: 417 passed, 219 subtests passed.
There was a problem hiding this comment.
🔵 Needs a closer look
Import-time logging initialization currently resets/detaches any preconfigured application handlers on the pcapkit logger, which breaks the stated “library-neutral import” contract.
Review details
Suppressed comments (6)
pcapkit/utilities/logging.py:180
- The ensure_output() docstring still claims it primarily exists for Extractor(verbose=True), but verbose output is now explicitly kept on print/stdout. Update this wording so it matches the current behavior and doesn’t suggest ensure_output affects verbose frame printing.
This exists for the features a caller switches on precisely *because* they
want to see the output -- ``Extractor(verbose=True)`` above all -- where
staying silent because the application never configured :mod:`logging` would
be a bug rather than good library manners.
pcapkit/foundation/extraction.py:19
- Unused import: logging is imported but never referenced in this module.
import logging
pcapkit/foundation/engines/scapy.py:13
- Unused import: logging is imported but never referenced in this module.
import logging
pcapkit/foundation/engines/pyshark.py:13
- Unused import: logging is imported but never referenced in this module.
import logging
pcapkit/foundation/engines/dpkt.py:13
- Unused import: logging is imported but never referenced in this module.
import logging
pcapkit/utilities/logging.py:332
- Calling reset() at import time detaches any handlers (and propagate/level settings) that the host application may have configured on the 'pcapkit' logger before importing this module, which contradicts the stated library-neutral import contract. Prefer idempotent initialisation that only adds a NullHandler when the logger has no handlers, and only adds the devmode stderr handler if it is not already attached.
# Re-running this module (which the test suite does, to exercise import-time
# behaviour) must not stack a second handler onto the process-wide ``pcapkit``
# logger, so start from a known-clean state rather than adding to whatever is
# already there.
reset()
- Files reviewed: 22/23 changed files
- Comments generated: 0 new
- Review effort level: Lite
Import called reset(), which detached whatever handlers the host application had attached to the pcapkit logger and forced NOTSET and propagate back on. That contradicts the point of the change: a library should inherit the application's configuration, not overwrite it because it was imported second. Import now only guarantees the logger has a handler, and the devmode branch checks membership before attaching, so re-execution still cannot stack handlers - which is what reset() was there for. Verified: an application that configures a handler, INFO and propagate=False before importing pcapkit keeps all three, and two reloads leave one handler. Also from review: - dropped `import logging` from four modules where reverting the verbose handlers to print left it unused (AST-verified, not just grepped: the name survives only in docstrings). - extraction and traceflow logged dumper.__name__, which make_dumper() always names 'DictDumper' whatever the format. They log the wrapped output class now. dictdumper exposes `kind` rather than `name`, but it is an instance property and both sites log before instantiation - and its value duplicates the `fmt` already on the same line, so the class name is what adds anything. - ensure_output's docstring no longer cites verbose= as its motivation, since that path deliberately stays on print, and notes nothing in pcapkit calls it. Full suite 529 passed, 4 skipped, 303 subtests; CI selection without generated captures 417 passed.
There was a problem hiding this comment.
🟡 Changes recommended
pcapkit.utilities.logging.configure() can unexpectedly detach existing application handlers by default and the devmode bootstrap can stack duplicate stderr handlers on module re-execution.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
pcapkit/utilities/logging.py:310
configure()currently detaches existing handlers wheneverreplace=True(the default), even if the caller is only adjustinglevelorpropagate. That can unexpectedly remove an application's previously configured handlers on thepcapkitlogger, contradicting the stated “only the arguments supplied take effect” behaviour and making targeted runtime adjustments destructive.
- Files reviewed: 22/23 changed files
- Comments generated: 1
- Review effort level: Lite
The note shipped in #384 said the double-emission and the global filter mutation were deliberately left alone; this branch fixes both, so it now states the model and defers to docs/source/pcapkit/utilities/warnings.rst.
Closes the Logging Integration item on the Help Wanted page, which asked to "fully extend [the logger's] functionality in the entire module".
What was wrong
pcapkit/utilities/logging.pyconfigured logging at import: one flatpcapkitlogger, aStreamHandler(sys.stderr)attached, level forced fromPCAPKIT_DEVMODE. Importing the library hijacked the consumer's logging, and undoing it meant reaching intologger.handlers. NoNullHandler.debugcalls, 36info, of which 34 were registry "registered X" bookkeeping. So the library shouted about its own housekeeping and said nothing about what it actually did. The whole ~200-filepcapkit/protocols/**parsing path had one call site.printcalls where logging was clearly intended, each self-documenting with a# pylint: disable=logging-fstring-interpolation.What this does
Library-citizen logging.
NullHandlerand no level at import, so verbosity is inherited from the application.PCAPKIT_DEVMODEkeeps the old stderr handler as the opt-in path. New API alongside the existingloggerobject:configureis orthogonal and additive — only the arguments passed take effect, so it works as one-shot setup and targeted adjustment, andname=is what makes the hierarchy usable (configure(WARNING, name='pcapkit.foundation.registry')). 17 modules now takegetLogger(__name__), so a consumer can silence the registry while keeping the extractor.Call sites rebalanced. 38
info→debug(34 registry, 4 extractor configuration), leaving exactly oneinfoin the package — thepcapkit-vendorCLI progress line, where it belongs. The fourprints becomelogger.debugwith lazy%args and the suppressions dropped. 31 newdebugcalls cover what was previously undiagnosable: extractor lifecycle (input open, engine requested vs used, magic-number dispatch, frame count, cleanup), engine selection and fallback, reassembly and trace-flow entry points. Nothing was added to per-frame or per-field loops — the traceflow entries sit inside per-flow branches, so they cost nothing per packet.The one judgement call, and how it landed
verbose=Trueand the CLI's-vprint a line per frame. Converting those tologger.debugmoved user-facing output onto the logger — changing its stream (stdout → stderr) and making it invisible until a consumer configured a handler.tests/integration/test_cli_subprocess.pycaught exactly that.So the four
verbose=handlers stay onprint: those chains are a feature of the tool, not diagnostics. That also removed the need forensure_output()on the library path — it existed only to stopverbose=Truefalling silent. The helper remains public and tested, since it is a reasonable thing for a consumer to call. Every genuinedebugaddition is untouched.Behaviour changes a consumer could notice
All documented in a compatibility note on the new docs page:
configure(logging.INFO, stream=sys.stderr).registered …messages moved todebug— invisible even after restoring anINFOhandler.handleris no longerlogger.handlers[0]— code indexing that list should callreset()/configure().Unaffected:
loggerstays public and namedpcapkit;PCAPKIT_DEVMODEbehaves as before;verbose=still prints to stdout.Docs
New
docs/source/pcapkit/utilities/logging.rst, matching the sibling per-submodule pages (functools,exceptions,warnings) — logging was the odd one inline in the index. Covers the hierarchy, whatDEBUGtells you, configuration recipes, the environment variables, and the compatibility note.pep.rst's Logging Integration proposal marked done in the same style as the Test Cases and New Engines entries.Out of scope, reported not changed
pcapkit/utilities/warnings.pyreports every warning twice (three times under devmode:warn()logs, thenwarnings.warnconstructs the category, whose__init__logs again), andBaseWarning.__init__callswarnings.simplefilter('ignore', ...)which mutates the process-global filter list. Filed as #363 and #364; both want their own change and their own test pass.pcapkit/utilities/exceptions.py'squiet=Truestill logging at ERROR is #362.Verification
Tests grown 3 → 41 in
tests/utilities/test_logging.py: import-time contract (NullHandler only,NOTSET, no stderr handler, devmode bootstrap, no handler stacking on re-execution), hierarchy andget_loggername mapping including__main__,configure/reset/ensure_outputbehaviour, registry bookkeeping atdebugviaassertLogs, the extractor debug trail, andverbose=printing to stdout while staying off the logger.Full suite: 529 passed, 4 skipped, 303 subtests passed.