Skip to content

Instantly share code, notes, and snippets.

@angstwad
Last active December 2, 2024 22:37
Show Gist options
  • Save angstwad/358c1cd937e933c01d3f72ad4abe2e7b to your computer and use it in GitHub Desktop.
Save angstwad/358c1cd937e933c01d3f72ad4abe2e7b to your computer and use it in GitHub Desktop.
Opinionated, standardized logging for Python CLIs
# An opinionated approach to logging in Python
import logging
import sys
def setup_logger(name: str | None = None,
level: str | int = 'WARN',
log_format: str | None = None) -> logging.Logger:
"""Sets up an opinionated logger bifurcating on log level to send
error and warning logs to stderr, info and debug to stdout
Args:
name: name of the logger to get; default `None` returns root logger
level: level attribute in logging as a string
log_format: optional log format; should be a valid format string,
see std lib logging docs for more info
Returns:
configured `logging.Logger`
"""
logger = logging.getLogger(name)
if logger.handlers:
return logger
if isinstance(level, int):
logger.setLevel(level)
else:
logger.setLevel(getattr(logging, level))
if log_format is None:
log_format = "%(name)-10s %(levelname)-7s %(message)s"
formatter = logging.Formatter(log_format)
h1 = logging.StreamHandler(sys.stdout)
h1.setLevel(logging.DEBUG)
h1.setFormatter(formatter)
h1.addFilter(lambda record: record.levelno <= logging.INFO)
h2 = logging.StreamHandler()
h2.setLevel(logging.WARNING)
h2.setFormatter(formatter)
logger.addHandler(h1)
logger.addHandler(h2)
return logger
class LoggingMixin:
""" Mixin class which adds logging functionality to a class. Not intended to be used as a
subclass. To use, call `self._setup_logger; this creates an instance-internal logger at
`self._logger`. When setting up a logger, this expects this app's primary logger, the
`cli` logger, to be already set up, and uses its level as the internal logger's level.
Then, call `self.log` as a convenience method or, alternatively, the internal logger directly.
"""
_logger: logging.Logger
def _setup_logger(self, name: str, msg_prefix: str | None = None) -> None:
"""Sets up an internal logger object using a prescriptive convention
Note that this tries to match the log level of the hydrate logger,
because that's what is set up using CLI args. Changes to the hydrate
logger will need to be set here
"""
self._prefix = msg_prefix
self._logger = setup_logger(name, level=logging.getLogger('cli').level)
def log(self, msg: str, lvl: str = 'error', **kwargs) -> None:
"""Convenience method for logging to the internal logger
Args:
msg: log message as a string
lvl: log level as a string name, should be a method name off a
`logging.Logger` object; i.e. `debug`, `info`, `warn`
Returns:
Result of logger method call; expected to be None
"""
meth = getattr(self._logger, lvl)
meth(f"{self._prefix + ": " if self._prefix else ""}{msg}", **kwargs)
# Copyright 2024 Paul Durivage <[email protected]>
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment