Skip to content

Logging

redsun logs everything to the redsun logger. Usage is in Configure logging.

Symbol What it does
set_level sets the level of the redsun logger
add_handler sends the logger's records to one more handler
remove_handler stops sending records to a handler
Loggable gives a component a logger that names the component in each record
BufferHandler keeps the most recent records of the session in memory
log_buffer returns the BufferHandler installed on the logger
SessionFileHandler writes the records of one run of a session to a rotated file
session_log returns the SessionFileHandler installed on the logger, or on a service's, if any
service_of names the service a record came from

The built-in LogView shows these records in the application.

Functions

set_level

set_level(level: int | str) -> None

Set the level of the redsun logger.

Level names are case-insensitive.

Raises:

Type Description
ValueError

If a name names no level.

Source code in src/redsun/log.py
def set_level(level: int | str) -> None:
    """Set the level of the ``redsun`` logger.

    Level names are case-insensitive.

    Raises
    ------
    ValueError
        If a name names no level.
    """
    logger.setLevel(level.upper() if isinstance(level, str) else level)

add_handler

add_handler(
    handler: Handler, service: str | None = None
) -> None

Send the redsun logger's records to handler as well.

With a service, only that service's records reach handler. A handler without a formatter gets the shared one, so records read the same everywhere.

Source code in src/redsun/log.py
def add_handler(handler: logging.Handler, service: str | None = None) -> None:
    """Send the ``redsun`` logger's records to *handler* as well.

    With a *service*, only that service's records reach *handler*. A handler
    without a formatter gets the shared one, so records read the same
    everywhere.
    """
    if handler.formatter is None:
        handler.setFormatter(GlobalFormatter(datefmt=DATE_FORMAT))
    _logger_for(service).addHandler(handler)

remove_handler

remove_handler(
    handler: Handler, service: str | None = None
) -> None

Stop sending the records add_handler sent to handler.

A handler that is not installed is left alone.

Source code in src/redsun/log.py
def remove_handler(handler: logging.Handler, service: str | None = None) -> None:
    """Stop sending the records `add_handler` sent to *handler*.

    A handler that is not installed is left alone.
    """
    _logger_for(service).removeHandler(handler)

log_buffer

log_buffer() -> BufferHandler

Return the buffer holding this session's log records.

Raises:

Type Description
RuntimeError

If the logging configuration no longer carries a buffer.

Source code in src/redsun/log.py
def log_buffer() -> BufferHandler:
    """Return the buffer holding this session's log records.

    Raises
    ------
    RuntimeError
        If the logging configuration no longer carries a buffer.
    """
    for handler in logger.handlers:
        if isinstance(handler, BufferHandler):
            return handler
    raise RuntimeError("no BufferHandler is installed on the 'redsun' logger")

session_log

session_log(
    service: str | None = None,
) -> SessionFileHandler | None

Return the handler writing this run's log file, if a session opened one.

With a service, the handler writing that service's file.

Source code in src/redsun/log.py
def session_log(service: str | None = None) -> SessionFileHandler | None:
    """Return the handler writing this run's log file, if a session opened one.

    With a *service*, the handler writing that service's file.
    """
    for handler in _logger_for(service).handlers:
        if isinstance(handler, SessionFileHandler):
            return handler
    return None

service_of

service_of(record: LogRecord) -> str | None

Return the name of the service record came from, None for the application.

Source code in src/redsun/log.py
def service_of(record: logging.LogRecord) -> str | None:
    """Return the name of the service *record* came from, ``None`` for the application."""
    prefix = f"{SERVICE_LOGGER}."
    if not record.name.startswith(prefix):
        return None
    return record.name.removeprefix(prefix).split(".", 1)[0]

Logging from a component

Mixin giving instances a logger that names them in each record.

Source code in src/redsun/log.py
class Loggable:
    """Mixin giving instances a logger that names them in each record."""

    @cached_property
    def logger(self) -> logging.LoggerAdapter[logging.Logger]:
        """Logger naming this instance in each record."""
        return ContextualAdapter(logging.getLogger("redsun"), self)

logger cached property

logger: LoggerAdapter[Logger]

Logger naming this instance in each record.

Session records

Bases: Handler

Retain the most recent log records, and announce each one as it arrives.

A consumer built later in the session can still show earlier records. Application records and each service's records are kept apart, each dropping its oldest when full, so a noisy service drops only its own.

Parameters:

Name Type Description Default
capacity int

How many application records to retain.

APPLICATION_CAPACITY
service_capacity int

How many records of each service to retain.

SERVICE_CAPACITY
Source code in src/redsun/log.py
class BufferHandler(logging.Handler):
    """Retain the most recent log records, and announce each one as it arrives.

    A consumer built later in the session can still show earlier records.
    Application records and each service's records are kept apart, each
    dropping its oldest when full, so a noisy service drops only its own.

    Parameters
    ----------
    capacity : int
        How many application records to retain.
    service_capacity : int
        How many records of each service to retain.
    """

    sig_record = Signal(logging.LogRecord)

    def __init__(
        self,
        capacity: int = APPLICATION_CAPACITY,
        service_capacity: int = SERVICE_CAPACITY,
    ) -> None:
        super().__init__()
        self._capacity = capacity
        self._service_capacity = service_capacity
        self._records: deque[logging.LogRecord] = deque(maxlen=capacity)
        self._service_records: dict[str, deque[logging.LogRecord]] = {}

    @property
    def capacity(self) -> int:
        """How many application records the buffer retains."""
        return self._capacity

    @property
    def service_capacity(self) -> int:
        """How many records of each service the buffer retains."""
        return self._service_capacity

    @property
    def records(self) -> tuple[logging.LogRecord, ...]:
        """The retained application records, oldest first."""
        return tuple(self._records)

    @property
    def services(self) -> tuple[str, ...]:
        """The services a record has come from, in the order they first did."""
        return tuple(self._service_records)

    def service_records(
        self, service: str | None = None
    ) -> tuple[logging.LogRecord, ...]:
        """Return one service's retained records, or every service's, oldest first."""
        if service is not None:
            return tuple(self._service_records.get(service, ()))
        return tuple(
            heapq.merge(*self._service_records.values(), key=lambda r: r.created)
        )

    def emit(self, record: logging.LogRecord) -> None:
        """Retain *record* with the records of its source, and announce it."""
        service = service_of(record)
        if service is None:
            self._records.append(record)
        else:
            self._service_records.setdefault(
                service, deque(maxlen=self._service_capacity)
            ).append(record)
        self.sig_record.emit(record)

    def clear(self) -> None:
        """Drop every retained record."""
        self._records.clear()
        self._service_records.clear()

capacity property

capacity: int

How many application records the buffer retains.

service_capacity property

service_capacity: int

How many records of each service the buffer retains.

records property

records: tuple[LogRecord, ...]

The retained application records, oldest first.

services property

services: tuple[str, ...]

The services a record has come from, in the order they first did.

service_records

service_records(
    service: str | None = None,
) -> tuple[logging.LogRecord, ...]

Return one service's retained records, or every service's, oldest first.

Source code in src/redsun/log.py
def service_records(
    self, service: str | None = None
) -> tuple[logging.LogRecord, ...]:
    """Return one service's retained records, or every service's, oldest first."""
    if service is not None:
        return tuple(self._service_records.get(service, ()))
    return tuple(
        heapq.merge(*self._service_records.values(), key=lambda r: r.created)
    )

emit

emit(record: LogRecord) -> None

Retain record with the records of its source, and announce it.

Source code in src/redsun/log.py
def emit(self, record: logging.LogRecord) -> None:
    """Retain *record* with the records of its source, and announce it."""
    service = service_of(record)
    if service is None:
        self._records.append(record)
    else:
        self._service_records.setdefault(
            service, deque(maxlen=self._service_capacity)
        ).append(record)
    self.sig_record.emit(record)

clear

clear() -> None

Drop every retained record.

Source code in src/redsun/log.py
def clear(self) -> None:
    """Drop every retained record."""
    self._records.clear()
    self._service_records.clear()

Bases: RotatingFileHandler

Write the records of one run of a session to a file of its own.

The file is in the user's log directory, in a folder named after the session, and named after the run: its start time and process. It rotates at LOG_MAX_BYTES, keeping LOG_BACKUPS older files.

The application's file, <run>.log, takes no service records, and opening it deletes the files of all but the session's LOG_RUNS_KEPT most recent runs. A service's file, <run>.<service>.log, belongs to run, is installed with add_handler(handler, service), and is created when the service first logs.

Parameters:

Name Type Description Default
session str

Name of the session.

required
service str | None

The service whose records the file holds, None for the application.

None
run str | None

The run, as the application handler's run. None starts a new run.

None
Source code in src/redsun/log.py
class SessionFileHandler(RotatingFileHandler):
    """Write the records of one run of a session to a file of its own.

    The file is in the user's log directory, in a folder named after the
    session, and named after the run: its start time and process. It rotates at
    `LOG_MAX_BYTES`, keeping `LOG_BACKUPS` older files.

    The application's file, ``<run>.log``, takes no service records, and
    opening it deletes the files of all but the session's `LOG_RUNS_KEPT` most
    recent runs. A service's file, ``<run>.<service>.log``, belongs to *run*,
    is installed with ``add_handler(handler, service)``, and is created when
    the service first logs.

    Parameters
    ----------
    session : str
        Name of the session.
    service : str | None
        The service whose records the file holds, ``None`` for the application.
    run : str | None
        The run, as the application handler's `run`. ``None`` starts a new
        run.
    """

    def __init__(
        self, session: str, service: str | None = None, run: str | None = None
    ) -> None:
        folder = Path(user_log_dir("redsun", appauthor=False)) / session_folder(session)
        folder.mkdir(parents=True, exist_ok=True)
        if run is None:
            started = datetime.now().astimezone().strftime("%Y-%m-%dT%H-%M-%S")
            run = f"{started}_{os.getpid()}"
        if service is None:
            _delete_old_runs(folder, keep=LOG_RUNS_KEPT - 1)
        self.run = run
        super().__init__(
            folder / (f"{run}.log" if service is None else f"{run}.{service}.log"),
            maxBytes=LOG_MAX_BYTES,
            backupCount=LOG_BACKUPS,
            encoding="utf-8",
            delay=service is not None,
        )
        if service is None:
            self.addFilter(lambda record: service_of(record) is None)

    @property
    def files(self) -> list[Path]:
        """The files of this run, oldest records first."""
        current = Path(self.baseFilename)
        rotated = [
            current.with_name(f"{current.name}.{index}")
            for index in range(self.backupCount, 0, -1)
        ]
        return [path for path in (*rotated, current) if path.exists()]

files property

files: list[Path]

The files of this run, oldest records first.