Skip to content
Closed
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
26 changes: 26 additions & 0 deletions docs/features/logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,26 @@ setup_logger(Path("build.log")) # console + file
build()
```

### Code location in the console

By default a console log line carries only the timestamp, the level and the message:

```text
2026-03-24 11:18:16.429 | INFO | building ...
```

Pass `show_code_location=True` to prefix every line with the module, the function and the line number that logged it. This helps while debugging and is noise in a build log:

```python
setup_logger(show_code_location=True)
```

```text
2026-03-24 11:18:16.429 | INFO | my_app.builder:build:12 - building ...
```

The log file is not affected by this option. A log file is read for debugging, so it always keeps the code location.

## Requirements

```{item} REQ-LOGGING_FILE-0.0.1 Print to file
Expand All @@ -29,6 +49,12 @@ build()
The user **shall** be able to specify the log file path.
```

```{item} REQ-LOGGING_CODE_LOCATION-0.0.1 Configurable code location

Print the code location (module, function and line number) in the console log only on request.
The user **shall** be able to enable or disable it. It **shall** be disabled by default.
```

```{item} REQ-LOGGING-2.0.0 Easy Setup and Use

Be easy to set up and use across all modules.
Expand Down
22 changes: 19 additions & 3 deletions src/py_app_dev/core/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
logger.level("START", no=38, color="<yellow>")
logger.level("STOP", no=39, color="<yellow>")

TIME_AND_LEVEL_FORMAT = "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | "
CODE_LOCATION_FORMAT = "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
MESSAGE_FORMAT = "<level>{message}</level>"

_R = TypeVar("_R")
_FuncType = Callable[..., _R]

Expand All @@ -43,13 +47,25 @@ def time_it(*args: Any, **kwargs: Any) -> _R:
return _time_it


@fulfills("REQ-LOGGING_CODE_LOCATION-0.0.1")
def create_log_format(show_code_location: bool = False) -> str:
"""Create the console log format, with or without the module, function and line number of the caller."""
code_location = CODE_LOCATION_FORMAT if show_code_location else ""
return f"{TIME_AND_LEVEL_FORMAT}{code_location}{MESSAGE_FORMAT}"


@fulfills("REQ-LOGGING_FILE-0.0.1")
def setup_logger(log_file: Path | None = None, clear: bool = True) -> None:
"""Setup logger to stdout and optionally to file."""
def setup_logger(log_file: Path | None = None, clear: bool = True, show_code_location: bool = False) -> None:
"""
Setup logger to stdout and optionally to file.

The code location is omitted from the console output unless `show_code_location` is set.
It is always kept in the log file, because a log file is read for debugging.
"""
logger.remove()
logger.add(
sys.stdout,
format="<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <level>{message}</level>",
format=create_log_format(show_code_location),
)
if log_file is not None:
logger.add(log_file, level="DEBUG")
Expand Down
40 changes: 39 additions & 1 deletion tests/test_logger.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,45 @@
from pathlib import Path

import pytest

from py_app_dev.core.docs_utils import validates
from py_app_dev.core.logging import log_to_file, logger, setup_logger, time_it
from py_app_dev.core.logging import create_log_format, log_to_file, logger, setup_logger, time_it


@pytest.fixture
def console_capture(capsys):
"""Capture the console log output and detach the sink afterwards, so that later tests do not write into a closed stream."""
yield capsys
logger.remove()


@pytest.mark.parametrize("show_code_location", [True, False])
@validates("REQ-LOGGING_CODE_LOCATION-0.0.1")
def test_create_log_format(show_code_location):
log_format = create_log_format(show_code_location)
assert ("{name}" in log_format) is show_code_location
assert ("{function}" in log_format) is show_code_location
assert ("{line}" in log_format) is show_code_location
assert "{message}" in log_format


@pytest.mark.parametrize("show_code_location", [True, False])
@validates("REQ-LOGGING_CODE_LOCATION-0.0.1")
def test_setup_logger_code_location(console_capture, show_code_location):
setup_logger(show_code_location=show_code_location)
logger.info("Detecting C compiler ABI info")
console_output = console_capture.readouterr().out
assert "Detecting C compiler ABI info" in console_output
assert (":test_setup_logger_code_location:" in console_output) is show_code_location


@validates("REQ-LOGGING_CODE_LOCATION-0.0.1")
def test_setup_logger_hides_code_location_by_default(console_capture):
setup_logger()
logger.info("Detecting C compiler ABI info")
console_output = console_capture.readouterr().out
assert "Detecting C compiler ABI info" in console_output
assert ":test_setup_logger_hides_code_location_by_default:" not in console_output


@validates("REQ-LOGGING_FILE-0.0.1")
Expand Down
Loading