Skip to content

Commit

Permalink
add f_lib.logging (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
ITProKyle committed Apr 3, 2024
1 parent e7523b7 commit 1e39af9
Show file tree
Hide file tree
Showing 33 changed files with 2,295 additions and 155 deletions.
18 changes: 18 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

[{*.{json,py},Makefile}]
indent_size = 4

[Makefile]
indent_style = tab

[{cdk,package,package-lock,tsconfig,tslint}.json]
indent_size = 2
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
.eggs/
.hypothesis/
.installed.cfg
.ipynb_checkpoints
.mypy_cache/
.pytest_cache/
.python-version
Expand Down
11 changes: 8 additions & 3 deletions .readthedocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@ build:
python: '3.11'
jobs:
post_create_environment:
# Install poetry
# https://python-poetry.org/docs/#installing-manually
- pip install poetry
- poetry config virtualenvs.create false
post_install:
- poetry self add "poetry-dynamic-versioning[plugin]"
- poetry install --with docs
post_install:
# Install dependencies with 'docs' dependency group
# https://python-poetry.org/docs/managing-dependencies/#dependency-groups
# VIRTUAL_ENV needs to be set manually for now.
# See https://github.com/readthedocs/readthedocs.org/pull/11152/
- VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH poetry install --with docs

# Build documentation in the docs/ directory with Sphinx
sphinx:
Expand Down
29 changes: 0 additions & 29 deletions .vscode/cspell.json
Original file line number Diff line number Diff line change
@@ -1,37 +1,8 @@
{
"allowCompoundWords": true,
"dictionaries": [
"python",
"softwareTerms"
],
"ignorePaths": [
"**/.git",
"**/.gitignore",
"**/package-lock.json",
"**/package.json",
"**/poetry.lock",
".vscode/extensions.json",
".vscode/settings.json"
],
"import": [
"@itprokyle/cspell-dict/cspell-ext.json"
],
"language": "en",
"languageSettings": [
{
"allowCompoundWords": true,
"dictionaries": [
"python"
],
"languageId": "toml"
},
{
"dictionaries": [
"bash"
],
"languageId": "*"
}
],
"maxNumberOfProblems": 100,
"version": "0.2",
"words": []
Expand Down
8 changes: 5 additions & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
{
"[python]": {
"editor.codeActionsOnSave": {
"source.fixAll.ruff": "explicit",
"source.organizeImports": "always"
},
"editor.defaultFormatter": "ms-python.black-formatter",
"editor.detectIndentation": false,
"editor.formatOnSave": true,
Expand Down Expand Up @@ -43,7 +47,5 @@
],
"restructuredtext.linter.doc8.extraArgs": [
"--config ${workspaceFolder}/pyproject.toml"
],
"restructuredtext.preview.scrollEditorWithPreview": false,
"restructuredtext.preview.scrollPreviewWithEditor": false
]
}
1 change: 1 addition & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
highlight_language = "default"
intersphinx_mapping = {
"python": ("https://docs.python.org/3", None), # link to python docs
"rich": ("https://rich.readthedocs.io/en/stable", None), # link to rich docs
}
language = "en"
master_doc = "index"
Expand Down
7 changes: 6 additions & 1 deletion f_lib/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
"""Finley library."""

import logging as __logging
from importlib.metadata import PackageNotFoundError, version

from . import aws, constants, mixins, utils
from . import aws, constants, logging, mixins, utils
from ._environment import Environment
from ._os_info import OsInfo
from ._system_info import SystemInfo, UnknownPlatformArchitectureError

# when creating loggers, always use instances of `f_lib.logging.Logger`
__logging.setLoggerClass(logging.Logger)

try:
__version__ = version(__name__)
except PackageNotFoundError: # cov: ignore
Expand All @@ -20,6 +24,7 @@
"SystemInfo",
"aws",
"constants",
"logging",
"mixins",
"utils",
"UnknownPlatformArchitectureError",
Expand Down
26 changes: 26 additions & 0 deletions f_lib/logging/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Logging utilities."""

from . import settings, utils
from ._console_handler import ConsoleHandler
from ._constants import DEFAULT_LOG_FORMAT, DEFAULT_LOG_FORMAT_VERBOSE, DEFAULT_STYLES
from ._extendable_highlighter import ExtendableHighlighter, HighlightTypedDict
from ._fluid_log_render import FluidLogRender
from ._log_level import LogLevel
from ._logger import Logger, LoggerSettings
from ._prefix_adaptor import PrefixAdaptor

__all__ = [
"DEFAULT_LOG_FORMAT",
"DEFAULT_LOG_FORMAT_VERBOSE",
"DEFAULT_STYLES",
"ConsoleHandler",
"ExtendableHighlighter",
"FluidLogRender",
"HighlightTypedDict",
"LogLevel",
"Logger",
"LoggerSettings",
"PrefixAdaptor",
"settings",
"utils",
]
165 changes: 165 additions & 0 deletions f_lib/logging/_console_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
"""Custom console :class:`~rich.logging.RichHandler`."""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING

from rich.logging import RichHandler
from rich.markup import escape
from rich.text import Text

if TYPE_CHECKING:
from collections.abc import Callable, Iterable
from datetime import datetime
from types import ModuleType

from rich.console import Console, ConsoleRenderable
from rich.highlighter import Highlighter

from ._fluid_log_render import FluidLogRender


class ConsoleHandler(RichHandler):
"""Custom console :class:`~rich.logging.RichHandler`."""

def __init__( # noqa: PLR0913
self,
level: int | str = logging.NOTSET,
console: Console | None = None,
*,
enable_link_path: bool = True,
highlighter: Highlighter | None = None,
keywords: list[str] | None = None,
locals_max_length: int = 10,
locals_max_string: int = 80,
log_render_kls: type[FluidLogRender] | None = None,
log_time_format: str | Callable[[datetime], Text] = "[%x %X]",
markup: bool = False,
name: str = "rich.console",
omit_repeated_times: bool = True,
rich_tracebacks: bool = False,
show_level: bool = True,
show_path: bool = True,
show_time: bool = True,
tracebacks_extra_lines: int = 3,
tracebacks_show_locals: bool = False,
tracebacks_suppress: Iterable[str | ModuleType] = (),
tracebacks_theme: str | None = None,
tracebacks_width: int | None = None,
tracebacks_word_wrap: bool = True,
**kwargs: object,
) -> None:
"""Instantiate class.
Args:
level: Log level.
console: Optional console instance to write logs.
Default will use a global console instance writing to stdout.
enable_link_path: Enable terminal link of path column to file.
highlighter: Highlighter to style log messages, or None to use ReprHighlighter.
keywords: List of words to highlight instead of ``RichHandler.KEYWORDS``.
locals_max_length: Maximum length of containers before abbreviating, or None for no abbreviation.
locals_max_string: Maximum length of string before truncating, or None to disable.
log_render_kls: Custom log rendering class.
If not provided, will use the one provided by rich.
log_time_format: If ``log_time`` is enabled, either string for strftime or callable that formats the time.
markup: Enable console markup in log messages.
name: Name of the handler. Can be used to check for existence.
omit_repeated_times: Omit repetition of the same time.
rich_tracebacks: Enable rich tracebacks with syntax highlighting and formatting.
show_level: Show a column for the level.
show_path: Show the path to the original log call.
show_time: Show a column for the time.
tracebacks_extra_lines: Additional lines of code to render tracebacks, or None for full width.
tracebacks_show_locals: Enable display of locals in tracebacks.
tracebacks_suppress: Optional sequence of modules or paths to exclude from traceback.
tracebacks_theme: Override pygments theme used in traceback.
tracebacks_width: Number of characters used to render tracebacks, or None for full width.
tracebacks_word_wrap: Enable word wrapping of long tracebacks lines.
**kwargs: Additional options added to :class:`~rich.logging.RichHandler`
that are not explicitly listed here. This is to provide support for future
releases without requiring a new release here to support it.
"""
super().__init__(
level,
console,
enable_link_path=enable_link_path,
highlighter=highlighter,
keywords=keywords,
locals_max_length=locals_max_length,
locals_max_string=locals_max_string,
log_time_format=log_time_format,
markup=markup,
omit_repeated_times=omit_repeated_times,
rich_tracebacks=rich_tracebacks,
show_level=show_level,
show_path=show_path,
show_time=show_time,
tracebacks_extra_lines=tracebacks_extra_lines,
tracebacks_show_locals=tracebacks_show_locals,
tracebacks_suppress=tracebacks_suppress,
tracebacks_theme=tracebacks_theme,
tracebacks_width=tracebacks_width,
tracebacks_word_wrap=tracebacks_word_wrap,
**kwargs,
)
if log_render_kls is not None:
self._log_render = log_render_kls(
omit_repeated_times=omit_repeated_times,
show_level=show_level,
show_path=show_path,
show_time=show_time,
)
self._name = name

def _determine_should_escape(self, record: logging.LogRecord) -> bool:
"""Determine if a log message should be passed to :function:`~rich.markup.escape`.
This can be overridden in subclasses for more control.
"""
return self._determine_use_markup(record) and getattr(record, "escape", False)

def _determine_use_markup(self, record: logging.LogRecord) -> bool:
"""Determine if markup should be used for a log record."""
return getattr(record, "markup", self.markup)

def render_message(self, record: logging.LogRecord, message: str) -> ConsoleRenderable:
"""Render message text in to Text.
Args:
record: logging Record.
message: String containing log message.
Returns:
ConsoleRenderable: Renderable to display log message.
"""
if self._determine_should_escape(record):
message = escape(message)
return super().render_message(*self._style_message(record, message))

def _style_message(
self,
record: logging.LogRecord,
message: str,
) -> tuple[logging.LogRecord, str]:
"""Apply style to the message."""
if not self._determine_use_markup(record):
return record, message
return record, Text.from_markup(message, style=record.levelname.lower()).markup

def get_level_text(self, record: logging.LogRecord) -> Text:
"""Get the level name from the record.
Args:
record: LogRecord instance.
Returns:
Text: A tuple of the style and level name.
"""
level_name = record.levelname
return Text.styled(f"[{level_name}]".ljust(9), f"logging.level.{level_name.lower()}")
39 changes: 39 additions & 0 deletions f_lib/logging/_constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Logging constants."""

from __future__ import annotations

from typing import TYPE_CHECKING

from rich.style import Style

if TYPE_CHECKING:
from collections.abc import Mapping

DEFAULT_LOG_FORMAT = "%(message)s"
"""Default log format."""

DEFAULT_LOG_FORMAT_VERBOSE = "%(name)s: %(message)s"
"""Default log format when a verbose log level is used."""

DEFAULT_STYLES: Mapping[str, Style] = {
"error": Style(color="red"),
"info": Style(),
"notice": Style(color="yellow"),
"success": Style(color="green"),
"warning": Style(color="orange1"),
"logging.level.critical": Style(color="red", bold=True, reverse=True),
"logging.level.debug": Style(color="green"),
"logging.level.error": Style(color="red", bold=True),
"logging.level.info": Style(color="blue"),
"logging.level.notice": Style(color="yellow"),
"logging.level.notset": Style(dim=True),
"logging.level.spam": Style(color="green", dim=True),
"logging.level.success": Style(color="green", bold=True),
"logging.level.verbose": Style(color="cyan"),
"logging.level.warning": Style(color="orange1"),
"logging.keyword": Style(bold=True, color="blue"),
"repr.brace": Style(),
"repr.call": Style(),
"repr.ellipsis": Style(dim=True, italic=True),
}
"""Default :class:`rich.style.Style` overrides."""
Loading

0 comments on commit 1e39af9

Please sign in to comment.