docs for muutils v0.6.17
View Source on GitHub

# muutils.logger.logger

logger with streams & levels, and a timer context manager

- `SimpleLogger` is an extremely simple logger that can write to both console and a file
- `Logger` class handles levels in a slightly different way than default python `logging`, and also has "streams" which allow for different sorts of output in the same logger this was mostly made with training models in mind and storing both metadata and loss
- `TimerContext` is a context manager that can be used to time the duration of a block of code

View Source
View on GitHub

      1"""logger with streams & levels, and a timer context manager
      2
      3- `SimpleLogger` is an extremely simple logger that can write to both console and a file
      4- `Logger` class handles levels in a slightly different way than default python `logging`,
      5        and also has "streams" which allow for different sorts of output in the same logger
      6        this was mostly made with training models in mind and storing both metadata and loss
      7- `TimerContext` is a context manager that can be used to time the duration of a block of code
      8"""
      9
     10from __future__ import annotations
     11
     12import json
     13import time
     14import typing
     15from functools import partial
     16from typing import Callable, Sequence
     17
     18from muutils.json_serialize import JSONitem, json_serialize
     19from muutils.logger.exception_context import ExceptionContext
     20from muutils.logger.headerfuncs import HEADER_FUNCTIONS, HeaderFunction
     21from muutils.logger.loggingstream import LoggingStream
     22from muutils.logger.simplelogger import AnyIO, SimpleLogger
     23
     24# pylint: disable=arguments-differ, bad-indentation, trailing-whitespace, trailing-newlines, unnecessary-pass, consider-using-with, use-dict-literal
     25
     26
     27def decode_level(level: int) -> str:
     28    if not isinstance(level, int):
     29        raise TypeError(f"level must be int, got {type(level) = } {level = }")
     30
     31    if level < -255:
     32        return f"FATAL_ERROR({level})"
     33    elif level < 0:
     34        return f"WARNING({level})"
     35    else:
     36        return f"INFO({level})"
     37
     38
     39# todo: add a context which catches and logs all exceptions
     40class Logger(SimpleLogger):
     41    """logger with more features, including log levels and streams
     42
     43    # Parameters:
     44            - `log_path : str | None`
     45            default log file path
     46            (defaults to `None`)
     47            - `log_file : AnyIO | None`
     48            default log io, should have a `.write()` method (pass only this or `log_path`, not both)
     49            (defaults to `None`)
     50            - `timestamp : bool`
     51            whether to add timestamps to every log message (under the `_timestamp` key)
     52            (defaults to `True`)
     53            - `default_level : int`
     54            default log level for streams/messages that don't specify a level
     55            (defaults to `0`)
     56            - `console_print_threshold : int`
     57            log level at which to print to the console, anything greater will not be printed unless overridden by `console_print`
     58            (defaults to `50`)
     59            - `level_header : HeaderFunction`
     60            function for formatting log messages when printing to console
     61            (defaults to `HEADER_FUNCTIONS["md"]`)
     62    - `keep_last_msg_time : bool`
     63            whether to keep the last message time
     64            (defaults to `True`)
     65
     66
     67    # Raises:
     68            - `ValueError` : _description_
     69    """
     70
     71    def __init__(
     72        self,
     73        log_path: str | None = None,
     74        log_file: AnyIO | None = None,
     75        default_level: int = 0,
     76        console_print_threshold: int = 50,
     77        level_header: HeaderFunction = HEADER_FUNCTIONS["md"],
     78        streams: dict[str | None, LoggingStream] | Sequence[LoggingStream] = (),
     79        keep_last_msg_time: bool = True,
     80        # junk args
     81        timestamp: bool = True,
     82        **kwargs,
     83    ):
     84        # junk arg checking
     85        # ==================================================
     86        if len(kwargs) > 0:
     87            raise ValueError(f"unrecognized kwargs: {kwargs}")
     88
     89        if not timestamp:
     90            raise ValueError(
     91                "timestamp must be True -- why would you not want timestamps?"
     92            )
     93
     94        # timing
     95        # ==================================================
     96        # timing compares
     97        self._keep_last_msg_time: bool = keep_last_msg_time
     98        # TODO: handle per stream?
     99        self._last_msg_time: float | None = time.time()
    100
    101        # basic setup
    102        # ==================================================
    103        # init BaseLogger
    104        super().__init__(log_file=log_file, log_path=log_path, timestamp=timestamp)
    105
    106        # level-related
    107        self._console_print_threshold: int = console_print_threshold
    108        self._default_level: int = default_level
    109
    110        # set up streams
    111        self._streams: dict[str | None, LoggingStream] = (
    112            streams
    113            if isinstance(streams, typing.Mapping)
    114            else {s.name: s for s in streams}
    115        )
    116        # default error stream
    117        if "error" not in self._streams:
    118            self._streams["error"] = LoggingStream(
    119                "error",
    120                aliases={
    121                    "err",
    122                    "except",
    123                    "Exception",
    124                    "exception",
    125                    "exceptions",
    126                    "errors",
    127                },
    128            )
    129
    130        # check alias duplicates
    131        alias_set: set[str | None] = set()
    132        for stream in self._streams.values():
    133            for alias in stream.aliases:
    134                if alias in alias_set:
    135                    raise ValueError(f"alias {alias} is already in use")
    136                alias_set.add(alias)
    137
    138        # add aliases
    139        for stream in tuple(self._streams.values()):
    140            for alias in stream.aliases:
    141                if alias not in self._streams:
    142                    self._streams[alias] = stream
    143
    144        # print formatting
    145        self._level_header: HeaderFunction = level_header
    146
    147        print({k: str(v) for k, v in self._streams.items()})
    148
    149    def _exception_context(
    150        self,
    151        stream: str = "error",
    152        # level: int = -256,
    153        # **kwargs,
    154    ) -> ExceptionContext:
    155        s: LoggingStream = self._streams[stream]
    156        return ExceptionContext(stream=s)
    157
    158    def log(  # type: ignore # yes, the signatures are different here.
    159        self,
    160        msg: JSONitem = None,
    161        lvl: int | None = None,
    162        stream: str | None = None,
    163        console_print: bool = False,
    164        extra_indent: str = "",
    165        **kwargs,
    166    ):
    167        """logging function
    168
    169        ### Parameters:
    170         - `msg : JSONitem`
    171           message (usually string or dict) to be logged
    172         - `lvl : int | None`
    173           level of message (lower levels are more important)
    174           (defaults to `None`)
    175         - `console_print : bool`
    176           override `console_print_threshold` setting
    177           (defaults to `False`)
    178         - `stream : str | None`
    179           whether to log to a stream (defaults to `None`), which logs to the default `None` stream
    180           (defaults to `None`)
    181        """
    182
    183        # add to known stream names if not present
    184        if stream not in self._streams:
    185            self._streams[stream] = LoggingStream(stream)
    186
    187        # set default level to either global or stream-specific default level
    188        # ========================================
    189        if lvl is None:
    190            if stream is None:
    191                lvl = self._default_level
    192            else:
    193                if self._streams[stream].default_level is not None:
    194                    lvl = self._streams[stream].default_level
    195                else:
    196                    lvl = self._default_level
    197
    198        assert lvl is not None, "lvl should not be None at this point"
    199
    200        # print to console with formatting
    201        # ========================================
    202        _printed: bool = False
    203        if console_print or (lvl <= self._console_print_threshold):
    204            # add some formatting
    205            print(
    206                self._level_header(
    207                    msg=msg,
    208                    lvl=lvl,
    209                    stream=stream,
    210                    extra_indent=extra_indent,
    211                )
    212            )
    213
    214            # store the last message time
    215            if self._last_msg_time is not None:
    216                self._last_msg_time = time.time()
    217
    218            _printed = True
    219
    220        # convert and add data
    221        # ========================================
    222        # converting to dict
    223        msg_dict: typing.Mapping
    224        if not isinstance(msg, typing.Mapping):
    225            msg_dict = {"_msg": msg}
    226        else:
    227            msg_dict = msg
    228
    229        # level+stream metadata
    230        if lvl is not None:
    231            msg_dict["_lvl"] = lvl
    232
    233        # msg_dict["_stream"] = stream # moved to LoggingStream
    234
    235        # extra data in kwargs
    236        if len(kwargs) > 0:
    237            msg_dict["_kwargs"] = kwargs
    238
    239        # add default contents (timing, etc)
    240        msg_dict = {
    241            **{k: v() for k, v in self._streams[stream].default_contents.items()},
    242            **msg_dict,
    243        }
    244
    245        # write
    246        # ========================================
    247        logfile_msg: str = json.dumps(json_serialize(msg_dict)) + "\n"
    248        if (
    249            (stream is None)
    250            or (stream not in self._streams)
    251            or (self._streams[stream].handler is None)
    252        ):
    253            # write to the main log file if no stream is specified
    254            self._log_file_handle.write(logfile_msg)
    255        else:
    256            # otherwise, write to the stream-specific file
    257            s_handler: AnyIO | None = self._streams[stream].handler
    258            if s_handler is not None:
    259                s_handler.write(logfile_msg)
    260            else:
    261                raise ValueError(
    262                    f"stream handler is None! something in the logging stream setup is wrong:\n{self}"
    263                )
    264
    265        # if it was important enough to print, flush all streams
    266        if _printed:
    267            self.flush_all()
    268
    269    def log_elapsed_last(
    270        self,
    271        lvl: int | None = None,
    272        stream: str | None = None,
    273        console_print: bool = True,
    274        **kwargs,
    275    ) -> float:
    276        """logs the time elapsed since the last message was printed to the console (in any stream)"""
    277        if self._last_msg_time is None:
    278            raise ValueError("no last message time!")
    279        else:
    280            return self.log(
    281                {"elapsed_time": round(time.time() - self._last_msg_time, 6)},
    282                lvl=(lvl if lvl is not None else self._console_print_threshold),
    283                stream=stream,
    284                console_print=console_print,
    285                **kwargs,
    286            )
    287
    288    def flush_all(self):
    289        """flush all streams"""
    290
    291        self._log_file_handle.flush()
    292
    293        for stream in self._streams.values():
    294            if stream.handler is not None:
    295                stream.handler.flush()
    296
    297    def __getattr__(self, stream: str) -> Callable:
    298        if stream.startswith("_"):
    299            raise AttributeError(f"invalid stream name {stream} (no underscores)")
    300        return partial(self.log, stream=stream)
    301
    302    def __getitem__(self, stream: str):
    303        return partial(self.log, stream=stream)
    304
    305    def __call__(self, *args, **kwargs):
    306        return self.log(*args, **kwargs)

def decode_level(level: int) -> str:
View Source
View on GitHub

    28def decode_level(level: int) -> str:
    29    if not isinstance(level, int):
    30        raise TypeError(f"level must be int, got {type(level) = } {level = }")
    31
    32    if level < -255:
    33        return f"FATAL_ERROR({level})"
    34    elif level < 0:
    35        return f"WARNING({level})"
    36    else:
    37        return f"INFO({level})"

class Logger(muutils.logger.simplelogger.SimpleLogger):
View Source
View on GitHub

     41class Logger(SimpleLogger):
     42    """logger with more features, including log levels and streams
     43
     44    # Parameters:
     45            - `log_path : str | None`
     46            default log file path
     47            (defaults to `None`)
     48            - `log_file : AnyIO | None`
     49            default log io, should have a `.write()` method (pass only this or `log_path`, not both)
     50            (defaults to `None`)
     51            - `timestamp : bool`
     52            whether to add timestamps to every log message (under the `_timestamp` key)
     53            (defaults to `True`)
     54            - `default_level : int`
     55            default log level for streams/messages that don't specify a level
     56            (defaults to `0`)
     57            - `console_print_threshold : int`
     58            log level at which to print to the console, anything greater will not be printed unless overridden by `console_print`
     59            (defaults to `50`)
     60            - `level_header : HeaderFunction`
     61            function for formatting log messages when printing to console
     62            (defaults to `HEADER_FUNCTIONS["md"]`)
     63    - `keep_last_msg_time : bool`
     64            whether to keep the last message time
     65            (defaults to `True`)
     66
     67
     68    # Raises:
     69            - `ValueError` : _description_
     70    """
     71
     72    def __init__(
     73        self,
     74        log_path: str | None = None,
     75        log_file: AnyIO | None = None,
     76        default_level: int = 0,
     77        console_print_threshold: int = 50,
     78        level_header: HeaderFunction = HEADER_FUNCTIONS["md"],
     79        streams: dict[str | None, LoggingStream] | Sequence[LoggingStream] = (),
     80        keep_last_msg_time: bool = True,
     81        # junk args
     82        timestamp: bool = True,
     83        **kwargs,
     84    ):
     85        # junk arg checking
     86        # ==================================================
     87        if len(kwargs) > 0:
     88            raise ValueError(f"unrecognized kwargs: {kwargs}")
     89
     90        if not timestamp:
     91            raise ValueError(
     92                "timestamp must be True -- why would you not want timestamps?"
     93            )
     94
     95        # timing
     96        # ==================================================
     97        # timing compares
     98        self._keep_last_msg_time: bool = keep_last_msg_time
     99        # TODO: handle per stream?
    100        self._last_msg_time: float | None = time.time()
    101
    102        # basic setup
    103        # ==================================================
    104        # init BaseLogger
    105        super().__init__(log_file=log_file, log_path=log_path, timestamp=timestamp)
    106
    107        # level-related
    108        self._console_print_threshold: int = console_print_threshold
    109        self._default_level: int = default_level
    110
    111        # set up streams
    112        self._streams: dict[str | None, LoggingStream] = (
    113            streams
    114            if isinstance(streams, typing.Mapping)
    115            else {s.name: s for s in streams}
    116        )
    117        # default error stream
    118        if "error" not in self._streams:
    119            self._streams["error"] = LoggingStream(
    120                "error",
    121                aliases={
    122                    "err",
    123                    "except",
    124                    "Exception",
    125                    "exception",
    126                    "exceptions",
    127                    "errors",
    128                },
    129            )
    130
    131        # check alias duplicates
    132        alias_set: set[str | None] = set()
    133        for stream in self._streams.values():
    134            for alias in stream.aliases:
    135                if alias in alias_set:
    136                    raise ValueError(f"alias {alias} is already in use")
    137                alias_set.add(alias)
    138
    139        # add aliases
    140        for stream in tuple(self._streams.values()):
    141            for alias in stream.aliases:
    142                if alias not in self._streams:
    143                    self._streams[alias] = stream
    144
    145        # print formatting
    146        self._level_header: HeaderFunction = level_header
    147
    148        print({k: str(v) for k, v in self._streams.items()})
    149
    150    def _exception_context(
    151        self,
    152        stream: str = "error",
    153        # level: int = -256,
    154        # **kwargs,
    155    ) -> ExceptionContext:
    156        s: LoggingStream = self._streams[stream]
    157        return ExceptionContext(stream=s)
    158
    159    def log(  # type: ignore # yes, the signatures are different here.
    160        self,
    161        msg: JSONitem = None,
    162        lvl: int | None = None,
    163        stream: str | None = None,
    164        console_print: bool = False,
    165        extra_indent: str = "",
    166        **kwargs,
    167    ):
    168        """logging function
    169
    170        ### Parameters:
    171         - `msg : JSONitem`
    172           message (usually string or dict) to be logged
    173         - `lvl : int | None`
    174           level of message (lower levels are more important)
    175           (defaults to `None`)
    176         - `console_print : bool`
    177           override `console_print_threshold` setting
    178           (defaults to `False`)
    179         - `stream : str | None`
    180           whether to log to a stream (defaults to `None`), which logs to the default `None` stream
    181           (defaults to `None`)
    182        """
    183
    184        # add to known stream names if not present
    185        if stream not in self._streams:
    186            self._streams[stream] = LoggingStream(stream)
    187
    188        # set default level to either global or stream-specific default level
    189        # ========================================
    190        if lvl is None:
    191            if stream is None:
    192                lvl = self._default_level
    193            else:
    194                if self._streams[stream].default_level is not None:
    195                    lvl = self._streams[stream].default_level
    196                else:
    197                    lvl = self._default_level
    198
    199        assert lvl is not None, "lvl should not be None at this point"
    200
    201        # print to console with formatting
    202        # ========================================
    203        _printed: bool = False
    204        if console_print or (lvl <= self._console_print_threshold):
    205            # add some formatting
    206            print(
    207                self._level_header(
    208                    msg=msg,
    209                    lvl=lvl,
    210                    stream=stream,
    211                    extra_indent=extra_indent,
    212                )
    213            )
    214
    215            # store the last message time
    216            if self._last_msg_time is not None:
    217                self._last_msg_time = time.time()
    218
    219            _printed = True
    220
    221        # convert and add data
    222        # ========================================
    223        # converting to dict
    224        msg_dict: typing.Mapping
    225        if not isinstance(msg, typing.Mapping):
    226            msg_dict = {"_msg": msg}
    227        else:
    228            msg_dict = msg
    229
    230        # level+stream metadata
    231        if lvl is not None:
    232            msg_dict["_lvl"] = lvl
    233
    234        # msg_dict["_stream"] = stream # moved to LoggingStream
    235
    236        # extra data in kwargs
    237        if len(kwargs) > 0:
    238            msg_dict["_kwargs"] = kwargs
    239
    240        # add default contents (timing, etc)
    241        msg_dict = {
    242            **{k: v() for k, v in self._streams[stream].default_contents.items()},
    243            **msg_dict,
    244        }
    245
    246        # write
    247        # ========================================
    248        logfile_msg: str = json.dumps(json_serialize(msg_dict)) + "\n"
    249        if (
    250            (stream is None)
    251            or (stream not in self._streams)
    252            or (self._streams[stream].handler is None)
    253        ):
    254            # write to the main log file if no stream is specified
    255            self._log_file_handle.write(logfile_msg)
    256        else:
    257            # otherwise, write to the stream-specific file
    258            s_handler: AnyIO | None = self._streams[stream].handler
    259            if s_handler is not None:
    260                s_handler.write(logfile_msg)
    261            else:
    262                raise ValueError(
    263                    f"stream handler is None! something in the logging stream setup is wrong:\n{self}"
    264                )
    265
    266        # if it was important enough to print, flush all streams
    267        if _printed:
    268            self.flush_all()
    269
    270    def log_elapsed_last(
    271        self,
    272        lvl: int | None = None,
    273        stream: str | None = None,
    274        console_print: bool = True,
    275        **kwargs,
    276    ) -> float:
    277        """logs the time elapsed since the last message was printed to the console (in any stream)"""
    278        if self._last_msg_time is None:
    279            raise ValueError("no last message time!")
    280        else:
    281            return self.log(
    282                {"elapsed_time": round(time.time() - self._last_msg_time, 6)},
    283                lvl=(lvl if lvl is not None else self._console_print_threshold),
    284                stream=stream,
    285                console_print=console_print,
    286                **kwargs,
    287            )
    288
    289    def flush_all(self):
    290        """flush all streams"""
    291
    292        self._log_file_handle.flush()
    293
    294        for stream in self._streams.values():
    295            if stream.handler is not None:
    296                stream.handler.flush()
    297
    298    def __getattr__(self, stream: str) -> Callable:
    299        if stream.startswith("_"):
    300            raise AttributeError(f"invalid stream name {stream} (no underscores)")
    301        return partial(self.log, stream=stream)
    302
    303    def __getitem__(self, stream: str):
    304        return partial(self.log, stream=stream)
    305
    306    def __call__(self, *args, **kwargs):
    307        return self.log(*args, **kwargs)

logger with more features, including log levels and streams

# Parameters:

        - `log_path : str | None`
        default log file path
        (defaults to `None`)
        - `log_file : AnyIO | None`
        default log io, should have a `.write()` method (pass only this or `log_path`, not both)
        (defaults to `None`)
        - `timestamp : bool`
        whether to add timestamps to every log message (under the `_timestamp` key)
        (defaults to `True`)
        - `default_level : int`
        default log level for streams/messages that don't specify a level
        (defaults to `0`)
        - `console_print_threshold : int`
        log level at which to print to the console, anything greater will not be printed unless overridden by `console_print`
        (defaults to `50`)
        - `level_header : HeaderFunction`
        function for formatting log messages when printing to console
        (defaults to `HEADER_FUNCTIONS["md"]`)

- `keep_last_msg_time : bool` whether to keep the last message time (defaults to `True`)

# Raises:

        - `ValueError` : _description_

Logger( log_path: str | None = None, log_file: Union[TextIO, muutils.logger.simplelogger.NullIO, NoneType] = None, default_level: int = 0, console_print_threshold: int = 50, level_header: muutils.logger.headerfuncs.HeaderFunction = <function md_header_function>, streams: Union[dict[str | None, muutils.logger.LoggingStream], Sequence[muutils.logger.LoggingStream]] = (), keep_last_msg_time: bool = True, timestamp: bool = True, **kwargs)
View Source
View on GitHub

     72    def __init__(
     73        self,
     74        log_path: str | None = None,
     75        log_file: AnyIO | None = None,
     76        default_level: int = 0,
     77        console_print_threshold: int = 50,
     78        level_header: HeaderFunction = HEADER_FUNCTIONS["md"],
     79        streams: dict[str | None, LoggingStream] | Sequence[LoggingStream] = (),
     80        keep_last_msg_time: bool = True,
     81        # junk args
     82        timestamp: bool = True,
     83        **kwargs,
     84    ):
     85        # junk arg checking
     86        # ==================================================
     87        if len(kwargs) > 0:
     88            raise ValueError(f"unrecognized kwargs: {kwargs}")
     89
     90        if not timestamp:
     91            raise ValueError(
     92                "timestamp must be True -- why would you not want timestamps?"
     93            )
     94
     95        # timing
     96        # ==================================================
     97        # timing compares
     98        self._keep_last_msg_time: bool = keep_last_msg_time
     99        # TODO: handle per stream?
    100        self._last_msg_time: float | None = time.time()
    101
    102        # basic setup
    103        # ==================================================
    104        # init BaseLogger
    105        super().__init__(log_file=log_file, log_path=log_path, timestamp=timestamp)
    106
    107        # level-related
    108        self._console_print_threshold: int = console_print_threshold
    109        self._default_level: int = default_level
    110
    111        # set up streams
    112        self._streams: dict[str | None, LoggingStream] = (
    113            streams
    114            if isinstance(streams, typing.Mapping)
    115            else {s.name: s for s in streams}
    116        )
    117        # default error stream
    118        if "error" not in self._streams:
    119            self._streams["error"] = LoggingStream(
    120                "error",
    121                aliases={
    122                    "err",
    123                    "except",
    124                    "Exception",
    125                    "exception",
    126                    "exceptions",
    127                    "errors",
    128                },
    129            )
    130
    131        # check alias duplicates
    132        alias_set: set[str | None] = set()
    133        for stream in self._streams.values():
    134            for alias in stream.aliases:
    135                if alias in alias_set:
    136                    raise ValueError(f"alias {alias} is already in use")
    137                alias_set.add(alias)
    138
    139        # add aliases
    140        for stream in tuple(self._streams.values()):
    141            for alias in stream.aliases:
    142                if alias not in self._streams:
    143                    self._streams[alias] = stream
    144
    145        # print formatting
    146        self._level_header: HeaderFunction = level_header
    147
    148        print({k: str(v) for k, v in self._streams.items()})

def log( self, msg: Union[bool, int, float, str, list, Dict[str, Any], NoneType] = None, lvl: int | None = None, stream: str | None = None, console_print: bool = False, extra_indent: str = '', **kwargs):
View Source
View on GitHub

    159    def log(  # type: ignore # yes, the signatures are different here.
    160        self,
    161        msg: JSONitem = None,
    162        lvl: int | None = None,
    163        stream: str | None = None,
    164        console_print: bool = False,
    165        extra_indent: str = "",
    166        **kwargs,
    167    ):
    168        """logging function
    169
    170        ### Parameters:
    171         - `msg : JSONitem`
    172           message (usually string or dict) to be logged
    173         - `lvl : int | None`
    174           level of message (lower levels are more important)
    175           (defaults to `None`)
    176         - `console_print : bool`
    177           override `console_print_threshold` setting
    178           (defaults to `False`)
    179         - `stream : str | None`
    180           whether to log to a stream (defaults to `None`), which logs to the default `None` stream
    181           (defaults to `None`)
    182        """
    183
    184        # add to known stream names if not present
    185        if stream not in self._streams:
    186            self._streams[stream] = LoggingStream(stream)
    187
    188        # set default level to either global or stream-specific default level
    189        # ========================================
    190        if lvl is None:
    191            if stream is None:
    192                lvl = self._default_level
    193            else:
    194                if self._streams[stream].default_level is not None:
    195                    lvl = self._streams[stream].default_level
    196                else:
    197                    lvl = self._default_level
    198
    199        assert lvl is not None, "lvl should not be None at this point"
    200
    201        # print to console with formatting
    202        # ========================================
    203        _printed: bool = False
    204        if console_print or (lvl <= self._console_print_threshold):
    205            # add some formatting
    206            print(
    207                self._level_header(
    208                    msg=msg,
    209                    lvl=lvl,
    210                    stream=stream,
    211                    extra_indent=extra_indent,
    212                )
    213            )
    214
    215            # store the last message time
    216            if self._last_msg_time is not None:
    217                self._last_msg_time = time.time()
    218
    219            _printed = True
    220
    221        # convert and add data
    222        # ========================================
    223        # converting to dict
    224        msg_dict: typing.Mapping
    225        if not isinstance(msg, typing.Mapping):
    226            msg_dict = {"_msg": msg}
    227        else:
    228            msg_dict = msg
    229
    230        # level+stream metadata
    231        if lvl is not None:
    232            msg_dict["_lvl"] = lvl
    233
    234        # msg_dict["_stream"] = stream # moved to LoggingStream
    235
    236        # extra data in kwargs
    237        if len(kwargs) > 0:
    238            msg_dict["_kwargs"] = kwargs
    239
    240        # add default contents (timing, etc)
    241        msg_dict = {
    242            **{k: v() for k, v in self._streams[stream].default_contents.items()},
    243            **msg_dict,
    244        }
    245
    246        # write
    247        # ========================================
    248        logfile_msg: str = json.dumps(json_serialize(msg_dict)) + "\n"
    249        if (
    250            (stream is None)
    251            or (stream not in self._streams)
    252            or (self._streams[stream].handler is None)
    253        ):
    254            # write to the main log file if no stream is specified
    255            self._log_file_handle.write(logfile_msg)
    256        else:
    257            # otherwise, write to the stream-specific file
    258            s_handler: AnyIO | None = self._streams[stream].handler
    259            if s_handler is not None:
    260                s_handler.write(logfile_msg)
    261            else:
    262                raise ValueError(
    263                    f"stream handler is None! something in the logging stream setup is wrong:\n{self}"
    264                )
    265
    266        # if it was important enough to print, flush all streams
    267        if _printed:
    268            self.flush_all()

logging function

### Parameters:

- `msg : JSONitem` message (usually string or dict) to be logged
- `lvl : int | None` level of message (lower levels are more important) (defaults to `None`)
- `console_print : bool` override `console_print_threshold` setting (defaults to `False`)
- `stream : str | None` whether to log to a stream (defaults to `None`), which logs to the default `None` stream (defaults to `None`)

def log_elapsed_last( self, lvl: int | None = None, stream: str | None = None, console_print: bool = True, **kwargs) -> float:
View Source
View on GitHub

    270    def log_elapsed_last(
    271        self,
    272        lvl: int | None = None,
    273        stream: str | None = None,
    274        console_print: bool = True,
    275        **kwargs,
    276    ) -> float:
    277        """logs the time elapsed since the last message was printed to the console (in any stream)"""
    278        if self._last_msg_time is None:
    279            raise ValueError("no last message time!")
    280        else:
    281            return self.log(
    282                {"elapsed_time": round(time.time() - self._last_msg_time, 6)},
    283                lvl=(lvl if lvl is not None else self._console_print_threshold),
    284                stream=stream,
    285                console_print=console_print,
    286                **kwargs,
    287            )

logs the time elapsed since the last message was printed to the console (in any stream)

def flush_all(self):
View Source
View on GitHub

    289    def flush_all(self):
    290        """flush all streams"""
    291
    292        self._log_file_handle.flush()
    293
    294        for stream in self._streams.values():
    295            if stream.handler is not None:
    296                stream.handler.flush()

flush all streams