Source code for cashocs.log

# Copyright (C) 2020-2025 Fraunhofer ITWM and Sebastian Blauth
#
# This file is part of cashocs.
#
# cashocs is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# cashocs is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with cashocs.  If not, see <https://www.gnu.org/licenses/>.

"""Logging for cashocs."""

from __future__ import annotations

import datetime
import logging

import fenics


[docs] class Logger: """Base class for logging.""" def __init__(self, name: str) -> None: """Initializes the logger. Args: name (str): The name of the logger. """ h = logging.StreamHandler() h.setLevel(logging.INFO) self._handler = h self._log = logging.getLogger(name) self._log.addHandler(h) self._log.setLevel(logging.DEBUG) self._logfiles: dict[str, logging.FileHandler] = {} self._indent_level = 0 self._use_timestamp = False self._time_stack: list[datetime.datetime] = [] self._group_stack: list[str] = [] self._level_stack: list[int] = []
[docs] def log(self, level: int, message: str) -> None: """Use the logging functionality of the logger to log to (various) handlers. Args: level (int): The log level of the message, same as the ones used in the python logging module. message (str): The message that should be logged. """ if fenics.MPI.rank(fenics.MPI.comm_world) == 0: self._log.log(level, self._format(message)) fenics.MPI.barrier(fenics.MPI.comm_world)
[docs] def debug(self, message: str) -> None: """Issues a message at the debug level. Args: message (str): The message that should be logged. """ self.log(logging.DEBUG, message)
[docs] def info(self, message: str) -> None: """Issues a message at the info level. Args: message (str): The message that should be logged. """ self.log(logging.INFO, message)
[docs] def warning(self, message: str) -> None: """Issues a message at the warning level. Args: message (str): The message that should be logged. """ self.log(logging.WARNING, message)
[docs] def error(self, message: str) -> None: """Issues a message at the error level. Note that this does not raise an exception at the moment. Args: message (str): The message that should be logged. """ self.log(logging.ERROR, message)
[docs] def critical(self, message: str) -> None: """Issues a message at the critical level. Note that this does not raise an exception at the moment. Args: message (str): The message that should be logged. """ self.log(logging.CRITICAL, message)
[docs] def begin(self, message: str, level: int = logging.INFO) -> None: """This signals the beginning of a (timed) block of logs. This is closed with a suitable call to :py:func:`cashocs.log.end` with which each call to :py:func:`cashocs.log.begin` has to be accompanied by. Args: message (str): The message indicating what block is started. level (int, optional): The log level used for issuing the messages. Defaults to logging.INFO. """ self._push_message(message) self._push_level(level) self._push_time() start_message = "Start: " + message self.log(level, start_message) self.log(level, "-" * len(start_message)) self._add_indent()
[docs] def end(self) -> None: """This signals the end of a block started with :py:func:`cashocs.log.begin`.""" elapsed_time = self._pop_time() message = self._pop_message() level = self._pop_level() self._add_indent(-1) end_message = "Finish: " + message + f" -- Elapsed time: {elapsed_time}\n" self.log(level, end_message)
def _add_indent(self, increment: int = 1) -> None: """This method adds an indent to the log. Args: increment (int, optional): The amount of indenting to do. Typically is +1 or -1. Defaults to 1. """ self._indent_level += increment
[docs] def set_log_level(self, level: int) -> None: """This method sets the log level of the default handler, i.e., the console. Args: level (int): The log level that should be used for the default handler. """ self._handler.setLevel(level)
def _push_time(self) -> None: """Pushes the current time to the time stack.""" self._time_stack.append(datetime.datetime.now()) def _pop_time(self) -> datetime.timedelta: """Generate a timedelta between the start and end time. Returns: datetime.timedelta: The timedelta between the :py:func:`cashocs.log.begin` and :py:func:`cashocs.log.end` calls. """ start_time = self._time_stack.pop() end_time = datetime.datetime.now() elapsed_time = end_time - start_time return elapsed_time def _push_message(self, message: str) -> None: """Pushes the message to the message stack. Args: message (str): The message that should be pushed to the stack. """ self._group_stack.append(message) def _pop_message(self) -> str: """Retrieves a message from the message stack. Returns: str: The message that was on the top of the message stack. """ return self._group_stack.pop() def _push_level(self, level: int) -> None: """Pushes the log level to the level stack. Args: level (int): The log level. """ self._level_stack.append(level) def _pop_level(self) -> int: """Retrieves the log level from the level stack. Returns: int: The log level that was on top of the level stack. """ return self._level_stack.pop() def _format(self, message: str) -> str: """Formats a message based on indent and time stamps for logging. Args: message (str): The input string which should be formatted. Returns: str: The formatted string. """ if self._use_timestamp: timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] timestamp = timestamp + " | " else: timestamp = "" indent = 2 * self._indent_level * " " return "\n".join([indent + timestamp + line for line in message.split("\n")])
[docs] def add_logfile( self, filename: str, mode: str = "a", level: int = logging.DEBUG ) -> logging.FileHandler: """Adds a file handler to the logger. Args: filename (str): The path to the file which is used for logging. mode (str, optional): The mode with which the log file should be treated. "a" appends to the file and "w" overwrites the file. Defaults to "a". level (int, optional): The log level used for logging to the file. Defaults to logging.DEBUG. Returns: logging.FileHandler: The file handler for the log file. """ if filename in self._logfiles: self.warning(f"Adding logfile {filename} multiple times.") return self._logfiles[filename] h = logging.FileHandler(filename, mode) h.setLevel(level) self._log.addHandler(h) self._logfiles[filename] = h return h
[docs] def add_handler(self, handler: logging.Handler) -> None: """Adds an additional handler to the logger. Args: handler (logging.Handler): The handler that should be added to the logger. """ self._log.addHandler(handler)
[docs] def add_timestamps(self) -> None: """This function adds a time stamp to the logged events.""" self._use_timestamp = True
[docs] def remove_timestamps(self) -> None: """This method removes the time stamp from the logged events.""" self._use_timestamp = False
cashocs_logger = Logger("cashocs") debug = cashocs_logger.debug info = cashocs_logger.info warning = cashocs_logger.warning error = cashocs_logger.error critical = cashocs_logger.critical begin = cashocs_logger.begin end = cashocs_logger.end set_log_level = cashocs_logger.set_log_level add_logfile = cashocs_logger.add_logfile add_timestamps = cashocs_logger.add_timestamps remove_timestamps = cashocs_logger.remove_timestamps add_handler = cashocs_logger.add_handler
[docs] class LogLevel: """Stores the various log levels of cashocs.""" DEBUG = logging.DEBUG INFO = logging.INFO WARNING = logging.WARNING ERROR = logging.ERROR CRITICAL = logging.CRITICAL
DEBUG = logging.DEBUG INFO = logging.INFO WARNING = logging.WARNING ERROR = logging.ERROR CRITICAL = logging.CRITICAL fenics.set_log_level(fenics.LogLevel.WARNING) logging.getLogger("UFL").setLevel(logging.WARNING) logging.getLogger("FFC").setLevel(logging.WARNING)