Basic logging utilities and filtering#925
Basic logging utilities and filtering#925iamhatesz wants to merge 3 commits intoroboflow:developfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces basic logging utilities to the supervision library, implementing standard library logging and warnings support as described in issue #835. The changes include a logger setup utility and a time-based filter for controlling log frequency.
Changes:
- Added
setup_logger()utility function to initialize supervision loggers with console output - Implemented
TimeBetweenOccurrencesFilterclass to throttle repeated log messages based on time intervals - Added comprehensive unit tests for the new logging functionality
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 9 comments.
| File | Description |
|---|---|
| supervision/utils/logging.py | Implements core logging utilities including setup_logger function and TimeBetweenOccurrencesFilter class |
| test/utils/test_logging.py | Provides unit tests for logger setup and time-based filtering functionality |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| from typing import Dict, Tuple | ||
|
|
||
|
|
||
| def setup_logger(**levels: int): |
There was a problem hiding this comment.
The setup_logger function is missing comprehensive documentation. Following the codebase patterns (e.g., functions in supervision/utils/file.py), this function should include a docstring with a description, Args section explaining the **levels parameter (what logger names are expected, what level values are valid), Returns section, and ideally an Examples section showing how to use it.
| def setup_logger(**levels: int): | |
| def setup_logger(**levels: int): | |
| """ | |
| Configure logging for the ``supervision`` package and optional named loggers. | |
| This helper sets up a console handler with a standard formatter and attaches | |
| it to the ``supervision`` root logger. By default, the ``supervision`` logger | |
| is configured with level :data:`logging.WARNING`. Additional logger levels | |
| can be configured via keyword arguments. | |
| Args: | |
| **levels: Mapping of logger names to logging levels. | |
| Each keyword argument name should be the name of a logger as used | |
| with :func:`logging.getLogger`, and each value must be a valid | |
| logging level accepted by the standard :mod:`logging` module. | |
| Typical values are the predefined constants | |
| :data:`logging.DEBUG`, :data:`logging.INFO`, | |
| :data:`logging.WARNING`, :data:`logging.ERROR`, or | |
| :data:`logging.CRITICAL`, or their corresponding integer values. | |
| For example, to change the level of the top-level ``"supervision"`` | |
| logger or one of its children such as ``"supervision.my_module"``, | |
| pass:: | |
| setup_logger(supervision=logging.INFO, | |
| supervision_my_module=logging.DEBUG) | |
| In this case the keyword names correspond to logger names that will | |
| be passed to :func:`logging.getLogger`. Ensure that the provided | |
| names match the loggers used in your application (e.g. | |
| ``"supervision"`` or ``"supervision.my_module"``). | |
| Returns: | |
| None: This function configures global logging state and does not | |
| return a value. | |
| Examples: | |
| Basic setup using default levels:: | |
| from supervision.utils.logging import setup_logger | |
| setup_logger() | |
| Configure specific logger levels:: | |
| import logging | |
| from supervision.utils.logging import setup_logger | |
| setup_logger( | |
| supervision=logging.INFO, | |
| supervision_detection=logging.DEBUG, | |
| ) | |
| After calling :func:`setup_logger`, loggers obtained via | |
| :func:`logging.getLogger` (for example, ``logging.getLogger("supervision")``) | |
| will use the configured levels and the shared console handler. | |
| """ |
|
|
||
|
|
||
| def function_that_logs(logger: logging.Logger, i: int): | ||
| logger.warning("Info message %d", i) |
There was a problem hiding this comment.
The naming is misleading here. The log message says "Info message" but it's actually being logged at the WARNING level. Consider changing the message to "Warning message" or something more descriptive to avoid confusion in tests.
| def filter(self, record: logging.LogRecord) -> bool: | ||
| record_id = _get_record_id(record) | ||
| now = datetime.fromtimestamp(record.created) | ||
| last_activity = self._last_activity.get(record_id) | ||
| self._last_activity[record_id] = now |
There was a problem hiding this comment.
The _last_activity dictionary is accessed and modified without thread synchronization. In multi-threaded environments where the same logger with this filter is used across multiple threads, this could lead to race conditions. Consider using threading.Lock to protect access to this shared state, or document that this filter is not thread-safe.
| captured = capsys.readouterr() | ||
| assert "Info message 1" not in captured.err | ||
|
|
||
| time.sleep(0.1) |
There was a problem hiding this comment.
The test uses time.sleep(0.1) to sleep for 100 milliseconds, which exactly matches the filter's timedelta(milliseconds=100). This creates a potential timing issue where the test could be flaky - if the sleep is slightly shorter than expected or if there's any overhead, the assertion may fail. Consider using a slightly longer sleep (e.g., 0.11 or 0.15 seconds) to make the test more robust.
| time.sleep(0.1) | |
| time.sleep(0.11) |
| super().__init__() | ||
| self._min_interval = min_interval | ||
|
|
||
| self._last_activity: Dict[_LogId, datetime] = {} |
There was a problem hiding this comment.
The _last_activity dictionary grows unbounded as it stores timestamps for every unique log location (pathname, lineno) encountered. In long-running applications with many different log calls, this could lead to memory accumulation. Consider implementing a cleanup mechanism (e.g., removing entries older than a certain threshold, or using a bounded cache like functools.lru_cache pattern) or documenting this behavior.
| class TimeBetweenOccurrencesFilter(logging.Filter): | ||
| """ | ||
| Adds filtering based on time elapsed between two same logging calls. | ||
| Useful to prevent displaying too much messages, e.g. in the loop. |
There was a problem hiding this comment.
The docstring contains a grammatical error. It should be "too many messages" instead of "too much messages" since "messages" is a countable noun.
| Useful to prevent displaying too much messages, e.g. in the loop. | |
| Useful to prevent displaying too many messages, e.g. in the loop. |
|
|
||
| def test_logging_without_setup_logger(capsys: pytest.CaptureFixture[str]): | ||
| logger = logging.getLogger("supervision.test_logging_without_setup_logger") | ||
| logger.warning("Info message") |
There was a problem hiding this comment.
The naming is misleading here. The log message says "Info message" but it's actually being logged at the WARNING level. Consider changing the message to "Warning message" or something more descriptive to avoid confusion in tests.
| def test_setup_logger(capsys: pytest.CaptureFixture[str]): | ||
| setup_logger() | ||
| logger = logging.getLogger("supervision.test_setup_logger") | ||
| logger.warning("Info message") |
There was a problem hiding this comment.
The naming is misleading here. The log message says "Info message" but it's actually being logged at the WARNING level. Consider changing the message to "Warning message" or something more descriptive to avoid confusion in tests.
| Adds filtering based on time elapsed between two same logging calls. | ||
| Useful to prevent displaying too much messages, e.g. in the loop. |
There was a problem hiding this comment.
The TimeBetweenOccurrencesFilter class documentation is incomplete. Following codebase patterns, the docstring should include an Args section describing the min_interval parameter, and ideally an Examples section demonstrating how to use this filter with a logger. The description should also clarify what "same logging calls" means (i.e., calls from the same file and line number).
| Adds filtering based on time elapsed between two same logging calls. | |
| Useful to prevent displaying too much messages, e.g. in the loop. | |
| Filter that limits how often log records from the same call site are emitted. | |
| Two log records are considered to come from the same call site if they have | |
| the same file path and line number (i.e., the same values of | |
| ``record.pathname`` and ``record.lineno``). For each such call site, log | |
| records are only allowed if at least ``min_interval`` has elapsed since the | |
| last emitted record from that location. | |
| This is useful to prevent flooding logs with repeated messages in tight | |
| loops or frequently executed code paths. | |
| Args: | |
| min_interval: Minimum allowed time between two emitted log records from | |
| the same file and line number, expressed as a ``datetime.timedelta``. | |
| Examples: | |
| Basic usage with a logger:: | |
| import logging | |
| from datetime import timedelta | |
| from supervision.utils.logging import TimeBetweenOccurrencesFilter | |
| logger = logging.getLogger(__name__) | |
| handler = logging.StreamHandler() | |
| handler.addFilter(TimeBetweenOccurrencesFilter( | |
| min_interval=timedelta(seconds=5) | |
| )) | |
| logger.addHandler(handler) | |
| # Within a tight loop, repeated messages from the same line will be | |
| # emitted at most once every 5 seconds. | |
| for i in range(1000): | |
| logger.info("Processing item") |
Borda
left a comment
There was a problem hiding this comment.
Is it used/initialized anywhere yet or it is just a tool? Anyway I believe a docs will be very welcoming...
Description
This is a PR solving issues described in #835
In summary:
loggingandwarningssetup_loggerutility to initializesupervisionloggersTimeBetweenOccurrencesFilterto use when we need to control the frequency of logging eventsType of change
Please delete options that are not relevant.
How has this change been tested, please provide a testcase or example of how you tested the change?
There are unit tests testing this functionality.
Any specific deployment considerations
Once we agree on the final interface, I'll make changes to the docs.
Docs