Skip to content

Logging

Logger that attempts to diagnose and propose a solution for any errors it is asked to log. Unlike our debugger and errors modules, explanations are not streamed because the intended use case is not focused on live development.

Quickstart

from roboduck import logging

logger = logging.getLogger(path='/tmp/log.txt')
data = {'x': 0}
try:
    x = data.x
except Exception as e:
    logger.error(e)

Classes

DuckLogger

Bases: Logger

Replacement for logging.Logger class that uses our errors module to log natural language explanations and fixes along with the original error. (More specifically, we just wait for the errors module to update the message in the original exception before logging.)

Source code in lib/roboduck/logging.py
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
class DuckLogger(Logger):
    """Replacement for logging.Logger class that uses our errors module to
    log natural language explanations and fixes along with the original error.
    (More specifically, we just wait for the errors module to update the
    message in the original exception before logging.)
    """

    def __init__(self, name, colordiff=False,
                 fmt='%(asctime)s [%(levelname)s]: %(message)s', stdout=True,
                 path='', fmode='a', **kwargs):
        """
        Parameters
        ----------
        name : str
            Same as base logger name arg.
        colordiff : bool
            Another kwarg to pass to our excepthook function. This is separate
            from the others because we want to use a different default than the
            function has since we often log to a file, in which case
            colordiff=True may be undesirable since color is only visible when
            printed.
        fmt : str
            Defines logging format. The default format produces output like
            this when an error is logged:
            2023-03-08 19:20:52,514 [ERROR]: list indices must be integers or
            slices, not tuple
        stdout : bool
            If True, logged items will appear in stdout. You are free to log
            to both stdout and a file OR just one (selecting neither will raise
            an error because logger would be useless in that case).
        path : str or Path
            If provided, we log to this file (the dir structure does not need
            to exist already). If None, we do not log to a file.
        fmode : str
            Write mode used when path is not None. Usually 'a' but 'w' might
            be a reasonable choice in some circumstances.
        kwargs : any
            Kwargs that can be passed to our excepthook function. Most of these
            should generally be kwargs for your debugger class,
            e.g. RoboDuckDb. These will be updated with the specified
            `colordiff` as well - we want to set the default to False here
             because we often want to log to a file, where this will probably
             not render correctly.
        """
        if not stdout and not path:
            raise RuntimeError(
                f'{type(self).__name__} requires that you set stdout=True '
                f'and/or provide a non-empty path. Currently, your logger '
                f'would do neither and be useless.'
            )

        super().__init__(name)
        self.excepthook_kwargs = kwargs or {}
        # Always want silent=True because we don't care about live typing here.
        # If stdout=True, our super()._log() call still ensures that we log to
        # stdout after the gpt call completes.
        defaults = dict(auto=True, sleep=0, silent=True, interactive=False)
        for k, v in defaults.items():
            if self.excepthook_kwargs.get(k, v) != v:
                warnings.warn(
                    f'You tried to set {k}={self.excepthook_kwargs[k]} '
                    f'but it must equal {v} in logger. Your arg will be'
                    f'overridden.'
                )
        self.excepthook_kwargs.update(defaults)
        self.excepthook_kwargs['colordiff'] = self.excepthook_kwargs.get(
            'colordiff', colordiff
        )
        self._add_handlers(fmt, stdout, path, fmode)

    def _add_handlers(self, fmt, stdout, path, fmode):
        """Set up handlers to log to stdout and/or a file."""
        formatter = Formatter(fmt)
        handlers = []
        if stdout:
            handlers.append(StreamHandler(sys.stdout))
        else:
            # If we don't set this when stdout is False, the root logger ends
            # up logging to stdout anyway.
            self.propagate = False

        if path:
            path = Path(path).resolve()
            os.makedirs(path.parent, exist_ok=True)
            handlers.append(FileHandler(path, fmode))
        for handler in handlers:
            handler.setFormatter(formatter)
            self.addHandler(handler)

    def _log(self, level, msg, args, exc_info=None, extra=None,
             stack_info=False):
        """This is where we insert our custom logic to get error explanations.
        We keep the import inside the method to avoid overwriting
        sys.excepthook whenever the logging module is imported.

        Low-level logging routine which creates a LogRecord and then calls
        all the handlers of this logger to handle the record.
        """
        if isinstance(msg, Exception) and sys.exc_info()[2]:
            from roboduck import errors
            errors.excepthook(type(msg), msg, msg.__traceback__,
                              **self.excepthook_kwargs)
            msg = sys.last_value
            errors.disable()

        return super()._log(level, msg, args, exc_info=exc_info,
                            extra=extra, stack_info=stack_info)

Attributes

excepthook_kwargs = kwargs or {} instance-attribute

Functions

getLogger(name=None, **kwargs)

Mimics interface of builtin logging.getLogger, but with our custom logger that ensures all errors explain themselves.

Parameters:

Name Type Description Default
kwargs any

These are passed to roboduck.errors.post_mortem which in turn passes them to our custom debugger class. The most important arg here is:

prompt_name (str) - determines what prompt/prompt_name the custom debugger uses, e.g. "debug_stack_trace". Users can also define their own custom prompt (https://hdmamin.github.io/roboduck/custom_prompts/) and pass in the file path here.

{}

Examples:

from roboduck import logging

logger = logging.getLogger()
Source code in lib/roboduck/logging.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
def getLogger(name=None, **kwargs):
    """Mimics interface of builtin logging.getLogger, but with our custom
    logger that ensures all errors explain themselves.

    Parameters
    ----------
    kwargs : any
        These are passed to roboduck.errors.post_mortem which in turn passes
        them to our custom debugger class. The most important arg here is:

        prompt_name (str) - determines what prompt/prompt_name the custom
            debugger uses, e.g. "debug_stack_trace". Users can also define
            their own custom prompt
            (https://hdmamin.github.io/roboduck/custom_prompts/)
            and pass in the file path here.

    Examples
    --------
    ```
    from roboduck import logging

    logger = logging.getLogger()
    ```
    """
    return DuckLogger(name=name, **kwargs)