Files
obsidian-metadata/src/obsidian_metadata/_utils/alerts.py
2023-03-03 21:10:43 -05:00

291 lines
7.6 KiB
Python

"""Logging and alerts."""
import sys
from enum import Enum
from pathlib import Path
from textwrap import wrap
import rich.repr
import typer
from loguru import logger
from obsidian_metadata._utils.console import console
class LogLevel(Enum):
"""Enum for log levels."""
TRACE = 5
DEBUG = 10
INFO = 20
SUCCESS = 25
WARNING = 30
ERROR = 40
CRITICAL = 50
EXCEPTION = 60
class VerboseLevel(Enum):
"""Enum for verbose levels."""
WARN = 0
INFO = 1
DEBUG = 2
TRACE = 3
def dryrun(msg: str) -> None:
"""Print a message if the dry run flag is set.
Args:
msg: Message to print
"""
console.print(f"[cyan]DRYRUN | {msg}[/cyan]")
def success(msg: str) -> None:
"""Print a success message without using logging.
Args:
msg: Message to print
"""
console.print(f"[green]SUCCESS | {msg}[/green]")
def warning(msg: str) -> None:
"""Print a warning message without using logging.
Args:
msg: Message to print
"""
console.print(f"[yellow]WARNING | {msg}[/yellow]")
def error(msg: str) -> None:
"""Print an error message without using logging.
Args:
msg: Message to print
"""
console.print(f"[red]ERROR | {msg}[/red]")
def notice(msg: str) -> None:
"""Print a notice message without using logging.
Args:
msg: Message to print
"""
console.print(f"[bold]NOTICE | {msg}[/bold]")
def info(msg: str) -> None:
"""Print a notice message without using logging.
Args:
msg: Message to print
"""
console.print(f"INFO | {msg}")
def usage(msg: str, width: int = 80) -> None:
"""Print a usage message without using logging.
Args:
msg: Message to print
width (optional): Width of the message
"""
for _n, line in enumerate(wrap(msg, width=width)):
if _n == 0:
console.print(f"[dim]USAGE | {line}")
else:
console.print(f"[dim] | {line}")
def debug(msg: str) -> None:
"""Print a debug message without using logging.
Args:
msg: Message to print
"""
console.print(f"[blue]DEBUG | {msg}[/blue]")
def dim(msg: str) -> None:
"""Print a message in dimmed color.
Args:
msg: Message to print
"""
console.print(f"[dim]{msg}[/dim]")
def _log_formatter(record: dict) -> str:
"""Create custom log formatter based on the log level. This effects the logs sent to stdout/stderr but not the log file."""
if (
record["level"].name == "INFO"
or record["level"].name == "SUCCESS"
or record["level"].name == "WARNING"
):
return "<level>{level: <8}</level> | <level>{message}</level>\n{exception}"
return "<level>{level: <8}</level> | <level>{message}</level> <fg #c5c5c5>({name}:{function}:{line})</fg #c5c5c5>\n{exception}"
@rich.repr.auto
class LoggerManager:
"""Instantiate the loguru logging system with the following levels.
- TRACE: Usage: log.trace("")
- DEBUG: Usage: log.debug("")
- INFO: Usage: log.info("")
- WARNING: Usage: log.warning("")
- ERROR: Usage: log.error("")
- CRITICAL: Usage: log.critical("")
- EXCEPTION: Usage: log.exception("")
Attributes:
log_file (Path): Path to the log file.
verbosity (int): Verbosity level.
log_to_file (bool): Whether to log to a file.
log_level (int): Default log level (verbosity overrides this)
Examples:
Instantiate the logger:
logging = _utils.alerts.LoggerManager(
verbosity,
log_to_file,
log_file,
log_level)
"""
def __init__(
self,
log_file: Path = Path("/logs"),
verbosity: int = 0,
log_to_file: bool = False,
log_level: int = 30,
) -> None:
self.verbosity = verbosity
self.log_to_file = log_to_file
self.log_file = log_file
self.log_level = log_level
if self.log_file == Path("/logs") and self.log_to_file: # pragma: no cover
console.print("No log file specified")
raise typer.Exit(1)
if self.verbosity >= VerboseLevel.TRACE.value:
logger.remove()
logger.add(
sys.stderr,
level="TRACE",
format=_log_formatter, # type: ignore[arg-type]
backtrace=False,
diagnose=True,
)
self.log_level = 5
elif self.verbosity == VerboseLevel.DEBUG.value:
logger.remove()
logger.add(
sys.stderr,
level="DEBUG",
format=_log_formatter, # type: ignore[arg-type]
backtrace=False,
diagnose=True,
)
self.log_level = 10
elif self.verbosity == VerboseLevel.INFO.value:
logger.remove()
logger.add(
sys.stderr,
level="INFO",
format=_log_formatter, # type: ignore[arg-type]
backtrace=False,
diagnose=True,
)
self.log_level = 20
else:
logger.remove()
logger.add(
sys.stderr,
format=_log_formatter, # type: ignore[arg-type]
level="WARNING",
backtrace=False,
diagnose=True,
)
self.log_level = 30
if self.log_to_file is True:
logger.add(
self.log_file,
rotation="5 MB",
level=self.log_level,
backtrace=False,
diagnose=True,
delay=True,
)
logger.debug(f"Logging to file: {self.log_file}")
logger.debug("Logging instantiated")
def is_trace(self, msg: str | None = None) -> bool:
"""Check if the current log level is TRACE.
Args:
msg (optional): Message to print. Defaults to None.
Returns:
bool: True if the current log level is TRACE or lower, False otherwise.
"""
if self.log_level <= LogLevel.TRACE.value:
if msg:
console.print(msg)
return True
return False
def is_debug(self, msg: str | None = None) -> bool:
"""Check if the current log level is DEBUG.
Args:
msg (optional): Message to print. Defaults to None.
Returns:
bool: True if the current log level is DEBUG or lower, False otherwise.
"""
if self.log_level <= LogLevel.DEBUG.value:
if msg:
console.print(msg)
return True
return False
def is_info(self, msg: str | None = None) -> bool:
"""Check if the current log level is INFO.
Args:
msg (optional): Message to print. Defaults to None.
Returns:
bool: True if the current log level is INFO or lower, False otherwise.
"""
if self.log_level <= LogLevel.INFO.value:
if msg:
console.print(msg)
return True
return False
def is_default(self, msg: str | None = None) -> bool:
"""Check if the current log level is default level (SUCCESS or WARNING).
Args:
msg (optional): Message to print. Defaults to None.
Returns:
bool: True if the current log level is default or lower, False otherwise.
"""
if self.log_level <= LogLevel.WARNING.value:
if msg:
console.print(msg)
return True
return False # pragma: no cover