Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to
- build:
- Support setuptools.packages.find.where directive in setup converter.
- Support setuptools.data-files directives in setup converter.
- log: Support log levels higher than INFO in `setup_logger()` (#89).

### Changed

Expand Down
50 changes: 50 additions & 0 deletions src/log/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,53 @@
# RFL: log package

Logging utility.

## setup_logger()

Configure the root logger with a stream handler, formatter, and per-component
filtering.

### Basic usage

```python
import logging
from rfl.log import setup_logger

setup_logger() # INFO level (default)
setup_logger(debug=True) # DEBUG level
setup_logger(level=logging.WARNING) # WARNING and above
setup_logger(level="ERROR") # string level names also accepted
```

### Log level

Level is resolved in this order:

1. `level` argument (int constant or string name: `DEBUG`, `INFO`, `WARNING`,
`ERROR`, `CRITICAL`)
2. `debug=True` → `DEBUG` (when `level` is omitted)
3. default → `INFO`

The `debug` flag also controls formatter verbosity (detailed prefixes), independent
of the resolved log level.

### Component filtering

By default, only log records from components listed in `log_flags` are shown for
INFO and above. DEBUG records require `debug_flags`.

```python
setup_logger(log_flags=["rfl"]) # INFO+ from rfl only
setup_logger(debug=True, debug_flags=["network"]) # DEBUG from network only
setup_logger(log_flags=["ALL"], debug_flags=["ALL"]) # show everything
```

Component names are derived from the first segment of the logger name
(e.g. `rfl.pkg.module` → `rfl`).

### Other options

- `component`: prefix log lines with a fixed component label in the formatter
- `formatter`: formatter class factory (default: TTY or daemon auto-selection)
- `clear`: when `True` (default), remove existing root handlers before setup;
set `clear=False` to stack handlers
20 changes: 17 additions & 3 deletions src/log/rfl/log/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#
# SPDX-License-Identifier: LGPL-3.0-or-later

from typing import Optional, List
from typing import Optional, List, Union
import logging

from .formatters import TTYFormatter, DaemonFormatter, auto_formatter
Expand All @@ -22,18 +22,32 @@

def setup_logger(
debug: bool = False,
level: Optional[Union[int, str]] = None,
log_flags: Optional[List[str]] = None,
debug_flags: Optional[List[str]] = None,
formatter: logging.Formatter = auto_formatter(),
component: Optional[str] = None,
clear: bool = True,
) -> None:
"""Setup root logger debug level, debug flags and formatter.
"""Setup root logger level, debug flags and formatter.

Log level is resolved in this order: explicit level argument, then
debug=True (DEBUG), otherwise INFO. The debug flag also controls
formatter verbosity, independent of the resolved log level.

When clear is True (default), existing root logger handlers are removed
before adding a new one, similarly to logging.basicConfig(force=True).
"""
if debug:
if level is not None:
if isinstance(level, str):
logging_level = getattr(logging, level.upper(), None)
if not isinstance(logging_level, int):
raise ValueError(f"Unknown log level: {level}")
elif isinstance(level, int):
logging_level = level
else:
raise TypeError(f"Unsupported log level type: {type(level).__name__}")
elif debug:
logging_level = logging.DEBUG
else:
logging_level = logging.INFO
Expand Down
36 changes: 36 additions & 0 deletions src/log/rfl/tests/test_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,42 @@ def test_setup_debug(self):
handler = logger.handlers[-1]
self.assertEqual(handler.level, logging.DEBUG)

def test_setup_warning(self):
setup_logger(level=logging.WARNING)
logger = logging.getLogger()
self.assertEqual(logger.level, logging.WARNING)
handler = logger.handlers[-1]
self.assertEqual(handler.level, logging.WARNING)

def test_setup_error(self):
setup_logger(level=logging.ERROR)
logger = logging.getLogger()
self.assertEqual(logger.level, logging.ERROR)
handler = logger.handlers[-1]
self.assertEqual(handler.level, logging.ERROR)

def test_setup_level_string(self):
setup_logger(level="WARNING")
logger = logging.getLogger()
self.assertEqual(logger.level, logging.WARNING)
handler = logger.handlers[-1]
self.assertEqual(handler.level, logging.WARNING)

def test_setup_level_overrides_debug(self):
setup_logger(level=logging.WARNING, debug=True)
logger = logging.getLogger()
self.assertEqual(logger.level, logging.WARNING)
handler = logger.handlers[-1]
self.assertEqual(handler.level, logging.WARNING)

def test_setup_invalid_level(self):
with self.assertRaisesRegex(ValueError, r"Unknown log level: NOTALEVEL"):
setup_logger(level="NOTALEVEL")

def test_setup_invalid_level_type(self):
with self.assertRaisesRegex(TypeError, r"Unsupported log level type: float"):
setup_logger(level=1.5)

def test_setup_filter(self):
setup_logger(log_flags=["rfl"])
# get root logger
Expand Down
Loading