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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
version, and creates a GitHub release. The process is documented in
`docs/releasing.md`. Publishing to PyPI is deliberately left out until the
distribution name is settled (#20).
- `setup_logging(use_emoji=...)` turns the level emoji in the verbose console
layout off (#25).

### Changed

Expand Down Expand Up @@ -119,3 +121,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
fields. Before, they had only the message and the context. The internal
`emoji` and `context` fields no longer leak into JSON, and the output is
identical across python-json-logger 2.x and later releases (#42).
- The emoji map listed levels that are never registered (`SYSTEM`, `SECURITY`,
`NETWORK`, `DATABASE`, `STARTUP`, `SHUTDOWN`) and could never match, so they
were removed. The level name is no longer followed by a stray space when a
level has no emoji (#25).
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ log.info("this one is carol's", extra={"user_id": "carol"})
| `to_console` | `True` | Log to the console. |
| `console_level` | `None` | Console level, defaulting to `level`. |
| `console_stream` | `None` | Console stream, for example `sys.stderr`. Defaults to `sys.stdout`. |
| `use_emoji` | `True` | Show a level emoji in the verbose console layout. Set `False` for terminals that render emoji badly. |
| `mode` | `"verbose"` | Console layout: `"verbose"` (coloured, with source location and context) or `"compact"` (`[LEVEL] message`). |
| `use_json` | `False` | Write JSON lines to the console instead. Needs the `json` extra. |
| `to_file` | `False` | Log to `file_path`. |
Expand Down
45 changes: 29 additions & 16 deletions pylogkit/log_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@
SYSLOG_FORMAT = "%(name)s[%(process)d]: %(levelname)s %(context)s - %(message)s"

# One colour per level, used for the level name and for the message text.
_LEVEL_EMOJI = {
"DEBUG": "🐛",
"INFO": "ℹ️",
"WARNING": "⚠️",
"ERROR": "❌",
"CRITICAL": "💥",
}

_LEVEL_COLOURS = {
"DEBUG": "cyan",
"INFO": "blue",
Expand Down Expand Up @@ -191,7 +199,7 @@ def _build_console_formatter(stream: IO[str] | None = None) -> SmartFieldFormatt
"""
return SmartFieldFormatter(
fmt=f"%({COLOUR_TIMESTAMP})s%(asctime)s%(reset)s "
"[%(log_color)s%(levelname)s %(emoji)s%(reset)s] "
"[%(log_color)s%(levelname)s%(emoji)s%(reset)s] "
f"[%({COLOUR_MODULE_NAME})s%(name)s%(reset)s] "
f"%({COLOUR_FILENAME})s%(filename)s%(reset)s::"
f"%({COLOUR_FUNCTION})s%(funcName)s%(reset)s():"
Expand All @@ -214,28 +222,29 @@ class ContextFilter(logging.Filter):
Values already present on the record (for example those supplied through
``extra=`` or by :class:`ContextualLoggerAdapter`) take precedence over
the global context.

Args:
use_emoji: Set ``record.emoji`` to the level's emoji (preceded by a
space, so that it lines up after the level name). When false the
field is empty.
"""

def __init__(self, use_emoji: bool = True) -> None:
"""Create the filter, optionally without level emoji."""
super().__init__()
self._use_emoji = use_emoji

def filter(self, record: logging.LogRecord) -> bool:
"""Add the context fields and emoji to ``record`` and accept it."""
context = get_log_context()
for k, v in context.items():
if not hasattr(record, k):
setattr(record, k, v)
emoji_map = {
"DEBUG": "🐛",
"INFO": "ℹ️",
"WARNING": "⚠️",
"ERROR": "❌",
"CRITICAL": "💥",
"SYSTEM": "🖥️",
"SECURITY": "🔐",
"NETWORK": "🌐",
"DATABASE": "🗄️",
"STARTUP": "🚀",
"SHUTDOWN": "🛑",
}
record.emoji = emoji_map.get(record.levelname, "")
record.emoji = (
f" {_LEVEL_EMOJI[record.levelname]}"
if self._use_emoji and record.levelname in _LEVEL_EMOJI
else ""
)
return True


Expand Down Expand Up @@ -365,6 +374,7 @@ def setup_logging(
syslog_address: tuple[str, int] | str = ("localhost", 514),
syslog_facility: int = SysLogHandler.LOG_USER,
console_stream: IO[str] | None = None,
use_emoji: bool = True,
) -> logging.Logger:
"""Configure and return a logger with console, file, JSON and syslog output.

Expand Down Expand Up @@ -413,6 +423,9 @@ def setup_logging(
Console colour is disabled when the stream is not a terminal, when
``NO_COLOR`` is set or ``TERM`` is ``dumb``, unless ``FORCE_COLOR``
is set.
use_emoji: Show a level emoji after the level name in the verbose
console layout. Set it to false for terminals and log viewers
that render emoji badly.

Returns:
The configured :class:`logging.Logger`.
Expand Down Expand Up @@ -476,7 +489,7 @@ def setup_logging(
syslog_handler.setFormatter(syslog_formatter)
logger.addHandler(syslog_handler)

context_filter = ContextFilter()
context_filter = ContextFilter(use_emoji=use_emoji)
for handler in logger.handlers:
handler.addFilter(context_filter)

Expand Down
44 changes: 44 additions & 0 deletions tests/test_emoji.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""Tests for the level emoji in the verbose console layout."""

import logging

from pylogkit import log_setup, setup_logging

STANDARD_LEVELS = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}


def test_emoji_map_only_names_registered_levels():
"""Every emoji entry belongs to a level that exists, so none is dead code."""
registered = {logging.getLevelName(n) for n in (10, 20, 30, 40, 50)}

assert set(log_setup._LEVEL_EMOJI) == STANDARD_LEVELS == registered


def test_emoji_is_shown_by_default(capsys):
"""The verbose layout puts the emoji right after the level name."""
setup_logging("emoji-on", level="DEBUG")

logging.getLogger("emoji-on").error("boom")

assert "[ERROR ❌]" in capsys.readouterr().out


def test_emoji_can_be_disabled(capsys):
"""Without emoji the level name is not followed by stray spacing."""
setup_logging("emoji-off", use_emoji=False)

logging.getLogger("emoji-off").error("boom")

out = capsys.readouterr().out
assert "[ERROR]" in out
assert "❌" not in out


def test_unknown_level_has_no_emoji_and_no_spacing(capsys):
"""A custom level prints cleanly."""
logging.addLevelName(25, "NOTICE")
setup_logging("emoji-custom", level="DEBUG")

logging.getLogger("emoji-custom").log(25, "custom")

assert "[NOTICE]" in capsys.readouterr().out
Loading