python-devlog 1.1__py3-none-any.whl → 2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- devlog/__init__.py +3 -4
- devlog/base.py +184 -0
- devlog/custom_excepthook.py +42 -28
- devlog/decorators.py +212 -0
- devlog/sanitize.py +139 -0
- python_devlog-2.0.dist-info/METADATA +227 -0
- python_devlog-2.0.dist-info/RECORD +10 -0
- {python_devlog-1.1.dist-info → python_devlog-2.0.dist-info}/WHEEL +1 -1
- devlog/decorator.py +0 -272
- devlog/stack_trace.py +0 -31
- python_devlog-1.1.dist-info/METADATA +0 -177
- python_devlog-1.1.dist-info/RECORD +0 -9
- {python_devlog-1.1.dist-info → python_devlog-2.0.dist-info/licenses}/LICENSE.txt +0 -0
- {python_devlog-1.1.dist-info → python_devlog-2.0.dist-info}/top_level.txt +0 -0
devlog/__init__.py
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
from .custom_excepthook import system_excepthook_overwrite
|
|
2
|
-
from .
|
|
3
|
-
from .
|
|
2
|
+
from .decorators import LogOnStart, LogOnError, LogOnEnd
|
|
3
|
+
from .sanitize import Sensitive
|
|
4
4
|
|
|
5
|
-
__all__ = ["log_on_start", "log_on_end", "log_on_error", "system_excepthook_overwrite", "
|
|
6
|
-
"set_stack_start_frames"]
|
|
5
|
+
__all__ = ["log_on_start", "log_on_end", "log_on_error", "system_excepthook_overwrite", "Sensitive"]
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
def log_on_start(*args, **kwargs):
|
devlog/base.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import inspect
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
import traceback
|
|
6
|
+
from functools import wraps
|
|
7
|
+
from logging import Logger, Handler
|
|
8
|
+
from types import FunctionType
|
|
9
|
+
from typing import Any, Callable, Dict, Optional, Set, Tuple, Type, Union
|
|
10
|
+
from warnings import warn
|
|
11
|
+
|
|
12
|
+
from .sanitize import Sensitive, unwrap_sensitive, format_value
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class WrapCallback:
|
|
16
|
+
r"""A callback that wraps the function and executes it.
|
|
17
|
+
|
|
18
|
+
This class is designed to be used as a mix-in for logging callables,
|
|
19
|
+
such as functions or methods. These are not created manually, instead
|
|
20
|
+
they are created from other log decorators in this package.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
# default execute wrapped function
|
|
24
|
+
def _devlog_executor(self, fn: FunctionType, *args: Any, **kwargs: Any) -> Any:
|
|
25
|
+
__tracebackhide__ = True
|
|
26
|
+
return fn(*args, **kwargs)
|
|
27
|
+
|
|
28
|
+
async def _async_devlog_executor(self, fn: FunctionType, *args: Any, **kwargs: Any) -> Any:
|
|
29
|
+
__tracebackhide__ = True
|
|
30
|
+
return await fn(*args, **kwargs)
|
|
31
|
+
|
|
32
|
+
def __call__(self, fn: FunctionType) -> Callable[..., Any]:
|
|
33
|
+
if asyncio.iscoroutinefunction(fn):
|
|
34
|
+
@wraps(fn)
|
|
35
|
+
async def devlog_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
36
|
+
__tracebackhide__ = True
|
|
37
|
+
return await self._async_devlog_executor(fn, *args, **kwargs)
|
|
38
|
+
else:
|
|
39
|
+
@wraps(fn)
|
|
40
|
+
def devlog_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
41
|
+
__tracebackhide__ = True
|
|
42
|
+
return self._devlog_executor(fn, *args, **kwargs)
|
|
43
|
+
|
|
44
|
+
return devlog_wrapper
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class LoggingDecorator(WrapCallback):
|
|
48
|
+
r"""A class that implements the protocol for a logging callable.
|
|
49
|
+
|
|
50
|
+
This class are responsible for create logging message for the function
|
|
51
|
+
and log it.
|
|
52
|
+
|
|
53
|
+
Attributes:
|
|
54
|
+
log_level: The log level to use for logging.
|
|
55
|
+
message: The message format for the log.
|
|
56
|
+
logger: The logger to use for logging.
|
|
57
|
+
If not set, the logger will be created using the module name of the function.
|
|
58
|
+
handler: The handler to use for logging.
|
|
59
|
+
callable_format_variable: The name of the variable to use for the callable.
|
|
60
|
+
args_kwargs: If True, the message will accept {args} {kwargs} format.
|
|
61
|
+
trace_stack: Whether to include the stack trace in the log.
|
|
62
|
+
capture_locals: Capture the locals of the function.
|
|
63
|
+
include_decorator: Whether to include the decorator in the trace log.
|
|
64
|
+
sanitize_params: Set of parameter names to auto-redact in logs.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
# Global default for sanitize_params
|
|
68
|
+
default_sanitize_params: Optional[Set[str]] = None
|
|
69
|
+
|
|
70
|
+
# Files to exclude from stack traces (populated by submodules)
|
|
71
|
+
_internal_files: Set[str] = set()
|
|
72
|
+
|
|
73
|
+
def __init__(self, log_level: int, message: str, *, logger: Optional[Logger] = None,
|
|
74
|
+
handler: Optional[Handler] = None, args_kwargs: bool = True,
|
|
75
|
+
callable_format_variable: str = "callable",
|
|
76
|
+
trace_stack: bool = False, capture_locals: bool = False,
|
|
77
|
+
include_decorator: bool = False,
|
|
78
|
+
sanitize_params: Optional[Set[str]] = None):
|
|
79
|
+
self.log_level = log_level
|
|
80
|
+
self.message = message
|
|
81
|
+
|
|
82
|
+
if logger is not None and handler is not None:
|
|
83
|
+
warn("logger and handler are both set, the handler will be ignored")
|
|
84
|
+
handler = None
|
|
85
|
+
|
|
86
|
+
self._logger = logger
|
|
87
|
+
self._handler = handler
|
|
88
|
+
|
|
89
|
+
self.callable_format_variable = callable_format_variable
|
|
90
|
+
self.include_decorator = include_decorator
|
|
91
|
+
self.trace_stack = trace_stack or capture_locals
|
|
92
|
+
self.capture_locals = capture_locals
|
|
93
|
+
self.args_kwargs = args_kwargs
|
|
94
|
+
self.sanitize_params = sanitize_params if sanitize_params is not None else self.default_sanitize_params
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def log(logger: Logger, log_level: int, msg: str) -> None:
|
|
98
|
+
logger.log(log_level, msg)
|
|
99
|
+
|
|
100
|
+
def get_logger(self, fn: FunctionType) -> Logger:
|
|
101
|
+
"""
|
|
102
|
+
Returns the logger to use for logging.
|
|
103
|
+
if the logger is not set, the logger will be created using the module name of the function.
|
|
104
|
+
and the handler will be added to the logger if any.
|
|
105
|
+
"""
|
|
106
|
+
if self._logger is None:
|
|
107
|
+
self._logger = logging.getLogger(fn.__module__)
|
|
108
|
+
|
|
109
|
+
if self._handler is not None:
|
|
110
|
+
self._logger.addHandler(self._handler)
|
|
111
|
+
|
|
112
|
+
return self._logger
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def _is_internal_file(cls, filename: str) -> bool:
|
|
116
|
+
return filename in cls._internal_files
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def get_stack_summary(cls, include_decorator: bool, *args: Any, **kwargs: Any):
|
|
120
|
+
stack = traceback.StackSummary.extract(traceback.walk_stack(None), *args, **kwargs)
|
|
121
|
+
stack.reverse()
|
|
122
|
+
|
|
123
|
+
for frame in stack:
|
|
124
|
+
if include_decorator or not cls._is_internal_file(frame.filename):
|
|
125
|
+
yield frame
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def bind_param(fn: FunctionType, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
129
|
+
"""
|
|
130
|
+
Returns a dictionary with all the `parameter`: `value` in the function.
|
|
131
|
+
"""
|
|
132
|
+
callable_signature = inspect.signature(fn)
|
|
133
|
+
bound_arguments = callable_signature.bind(*args, **kwargs)
|
|
134
|
+
bounded_param = {param_name: bound_arguments.arguments.get(param_name, param_object.default) for
|
|
135
|
+
param_name, param_object in bound_arguments.signature.parameters.items()}
|
|
136
|
+
|
|
137
|
+
return bounded_param
|
|
138
|
+
|
|
139
|
+
def _sanitize_bound_params(self, bound_params: Dict[str, Any]) -> Dict[str, str]:
|
|
140
|
+
"""Format bound params with sanitization applied."""
|
|
141
|
+
result = {}
|
|
142
|
+
for name, value in bound_params.items():
|
|
143
|
+
result[name] = format_value(value, name, self.sanitize_params)
|
|
144
|
+
return result
|
|
145
|
+
|
|
146
|
+
def build_msg(self, fn: FunctionType, fn_args: Any, fn_kwargs: Any, **extra: Any) -> str:
|
|
147
|
+
"""
|
|
148
|
+
Builds the message log using the message format and the function arguments.
|
|
149
|
+
"""
|
|
150
|
+
format_kwargs = extra
|
|
151
|
+
|
|
152
|
+
if self.args_kwargs:
|
|
153
|
+
# Sanitize args and kwargs for display
|
|
154
|
+
sanitized_args = tuple(
|
|
155
|
+
format_value(a, sanitize_params=self.sanitize_params) for a in fn_args
|
|
156
|
+
) if self.sanitize_params or any(isinstance(a, Sensitive) for a in fn_args) else fn_args
|
|
157
|
+
sanitized_kwargs = {
|
|
158
|
+
k: format_value(v, k, self.sanitize_params) for k, v in fn_kwargs.items()
|
|
159
|
+
} if self.sanitize_params or any(isinstance(v, Sensitive) for v in fn_kwargs.values()) else fn_kwargs
|
|
160
|
+
format_kwargs["args"] = sanitized_args
|
|
161
|
+
format_kwargs["kwargs"] = sanitized_kwargs
|
|
162
|
+
else:
|
|
163
|
+
bound = self.bind_param(fn, *fn_args, **fn_kwargs)
|
|
164
|
+
if self.sanitize_params or any(isinstance(v, Sensitive) for v in bound.values()):
|
|
165
|
+
format_kwargs.update(self._sanitize_bound_params(bound))
|
|
166
|
+
else:
|
|
167
|
+
format_kwargs.update(bound)
|
|
168
|
+
return self.message.format(**format_kwargs)
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def _unwrap_args(args: tuple, kwargs: dict) -> Tuple[tuple, dict]:
|
|
172
|
+
"""Unwrap Sensitive values before passing to the actual function."""
|
|
173
|
+
unwrapped_args = tuple(unwrap_sensitive(a) for a in args)
|
|
174
|
+
unwrapped_kwargs = {k: unwrap_sensitive(v) for k, v in kwargs.items()}
|
|
175
|
+
return unwrapped_args, unwrapped_kwargs
|
|
176
|
+
|
|
177
|
+
def _has_sensitive(self, args: tuple, kwargs: dict) -> bool:
|
|
178
|
+
"""Check if any args/kwargs contain Sensitive values."""
|
|
179
|
+
return any(isinstance(a, Sensitive) for a in args) or \
|
|
180
|
+
any(isinstance(v, Sensitive) for v in kwargs.values())
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# Register this file as internal
|
|
184
|
+
LoggingDecorator._internal_files.add(__file__)
|
devlog/custom_excepthook.py
CHANGED
|
@@ -1,33 +1,47 @@
|
|
|
1
|
+
import sys
|
|
1
2
|
import traceback
|
|
3
|
+
from typing import Optional, Type
|
|
4
|
+
from types import TracebackType
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
6
|
+
|
|
7
|
+
_output_file: str = "crash.log"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def my_except_hook(exception_type: Type[BaseException],
|
|
11
|
+
exception_value: BaseException,
|
|
12
|
+
traceback_message: Optional[TracebackType]) -> None:
|
|
13
|
+
"""Custom exception hook that writes crash info to a file and stdout."""
|
|
14
|
+
tb_exception = traceback.TracebackException(
|
|
15
|
+
exception_type, exception_value, traceback_message,
|
|
16
|
+
capture_locals=True
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
formatted = "".join(tb_exception.format())
|
|
20
|
+
|
|
21
|
+
# Print to stdout
|
|
22
|
+
print(formatted, end="")
|
|
23
|
+
|
|
24
|
+
# Write to file
|
|
25
|
+
try:
|
|
26
|
+
with open(_output_file, encoding="utf-8", mode="w") as f:
|
|
27
|
+
f.write(formatted)
|
|
28
|
+
f.write("\nStack (most recent stack last):\n")
|
|
29
|
+
for frame in tb_exception.stack:
|
|
30
|
+
message = "\t{filename}:{lineno} on {line}\n\t\t{locals}".format(
|
|
31
|
+
filename=frame.filename,
|
|
32
|
+
lineno=frame.lineno,
|
|
33
|
+
line=frame.line,
|
|
34
|
+
locals=frame.locals
|
|
35
|
+
)
|
|
36
|
+
f.write(message + "\n")
|
|
37
|
+
except OSError as e:
|
|
38
|
+
print(f"devlog: Failed to write crash log to {_output_file}: {e}", file=sys.stderr)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def system_excepthook_overwrite(out_file: Optional[str] = None) -> None:
|
|
42
|
+
"""Override sys.excepthook to write crash logs with local variable capture."""
|
|
43
|
+
global _output_file
|
|
30
44
|
|
|
31
45
|
if out_file is not None:
|
|
32
|
-
|
|
46
|
+
_output_file = out_file
|
|
33
47
|
sys.excepthook = my_except_hook
|
devlog/decorators.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
import traceback
|
|
5
|
+
from types import FunctionType
|
|
6
|
+
from typing import Any, Optional, Tuple, Type, Union
|
|
7
|
+
|
|
8
|
+
from .base import WrapCallback, LoggingDecorator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LogOnStart(LoggingDecorator):
|
|
12
|
+
r"""A logging decorator that logs the start of the function.
|
|
13
|
+
|
|
14
|
+
This decorator will log the start of the function using the logger and the handler
|
|
15
|
+
provided in the constructor.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
log_level: The log level to use for logging.
|
|
19
|
+
message: The message format for the log.
|
|
20
|
+
logger: The logger to use for logging.
|
|
21
|
+
If not set, the logger will be created using the module name of the function.
|
|
22
|
+
handler: The handler to use for logging.
|
|
23
|
+
callable_format_variable: The name of the variable to use for the callable.
|
|
24
|
+
args_kwargs: If True, the message will accept {args} {kwargs} format.
|
|
25
|
+
trace_stack: Whether to include the stack trace in the log.
|
|
26
|
+
trace_stack_message:The message format for the stack trace log.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, log_level: int = logging.INFO,
|
|
30
|
+
message: Optional[str] = None,
|
|
31
|
+
**kwargs: Any):
|
|
32
|
+
super().__init__(log_level, message, **kwargs)
|
|
33
|
+
if message is None:
|
|
34
|
+
self.message = "Start func {{{cal_var}.__name__}} " \
|
|
35
|
+
"with args {{args}}, kwargs {{kwargs}}".format(cal_var=self.callable_format_variable)
|
|
36
|
+
|
|
37
|
+
def _devlog_executor(self, fn: FunctionType, *args: Any, **kwargs: Any) -> Any:
|
|
38
|
+
__tracebackhide__ = True
|
|
39
|
+
self._do_logging(fn, *args, **kwargs)
|
|
40
|
+
if self._has_sensitive(args, kwargs):
|
|
41
|
+
args, kwargs = self._unwrap_args(args, kwargs)
|
|
42
|
+
return super()._devlog_executor(fn, *args, **kwargs)
|
|
43
|
+
|
|
44
|
+
async def _async_devlog_executor(self, fn: FunctionType, *args: Any, **kwargs: Any) -> Any:
|
|
45
|
+
__tracebackhide__ = True
|
|
46
|
+
self._do_logging(fn, *args, **kwargs)
|
|
47
|
+
if self._has_sensitive(args, kwargs):
|
|
48
|
+
args, kwargs = self._unwrap_args(args, kwargs)
|
|
49
|
+
return await super()._async_devlog_executor(fn, *args, **kwargs)
|
|
50
|
+
|
|
51
|
+
def _do_logging(self, fn: FunctionType, *args: Any, **kwargs: Any) -> None:
|
|
52
|
+
logger = self.get_logger(fn)
|
|
53
|
+
extra = {self.callable_format_variable: fn}
|
|
54
|
+
msg = self.build_msg(fn, fn_args=args, fn_kwargs=kwargs, **extra)
|
|
55
|
+
|
|
56
|
+
self.log(logger, self.log_level, msg)
|
|
57
|
+
if self.trace_stack:
|
|
58
|
+
stack = traceback.StackSummary(
|
|
59
|
+
LoggingDecorator.get_stack_summary(self.include_decorator, capture_locals=self.capture_locals)
|
|
60
|
+
)
|
|
61
|
+
self.log(logger, logging.DEBUG, "".join(stack.format()).strip())
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class LogOnEnd(LoggingDecorator):
|
|
65
|
+
r"""A logging decorator that logs the end of the function.
|
|
66
|
+
|
|
67
|
+
This decorator will log the end of the function using the logger and the handler
|
|
68
|
+
provided in the constructor.
|
|
69
|
+
|
|
70
|
+
Attributes:
|
|
71
|
+
log_level: The log level to use for logging.
|
|
72
|
+
message: The message format for the log.
|
|
73
|
+
logger: The logger to use for logging.
|
|
74
|
+
If not set, the logger will be created using the module name of the function.
|
|
75
|
+
handler: The handler to use for logging.
|
|
76
|
+
callable_format_variable: The name of the variable to use for the callable.
|
|
77
|
+
args_kwargs: If True, the message will accept {args} {kwargs} format.
|
|
78
|
+
trace_stack: Whether to include the stack trace in the log.
|
|
79
|
+
trace_stack_message:The message format for the stack trace log.
|
|
80
|
+
result_format_variable: The variable to use for the result.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self, log_level: int = logging.INFO,
|
|
84
|
+
message: Optional[str] = None, result_format_variable: str = "result",
|
|
85
|
+
**kwargs: Any):
|
|
86
|
+
super().__init__(log_level, message, **kwargs)
|
|
87
|
+
if message is None:
|
|
88
|
+
self.message = "Successfully run func {{{cal_var}.__name__}} " \
|
|
89
|
+
"with args {{args}}, kwargs {{kwargs}}".format(cal_var=self.callable_format_variable)
|
|
90
|
+
self.result_format_variable = result_format_variable
|
|
91
|
+
|
|
92
|
+
def _devlog_executor(self, fn: FunctionType, *args: Any, **kwargs: Any) -> Any:
|
|
93
|
+
__tracebackhide__ = True
|
|
94
|
+
original_args, original_kwargs = args, kwargs
|
|
95
|
+
if self._has_sensitive(args, kwargs):
|
|
96
|
+
args, kwargs = self._unwrap_args(args, kwargs)
|
|
97
|
+
result = super()._devlog_executor(fn, *args, **kwargs)
|
|
98
|
+
self._do_logging(fn, result, *original_args, **original_kwargs)
|
|
99
|
+
return result
|
|
100
|
+
|
|
101
|
+
async def _async_devlog_executor(self, fn: FunctionType, *args: Any, **kwargs: Any) -> Any:
|
|
102
|
+
__tracebackhide__ = True
|
|
103
|
+
original_args, original_kwargs = args, kwargs
|
|
104
|
+
if self._has_sensitive(args, kwargs):
|
|
105
|
+
args, kwargs = self._unwrap_args(args, kwargs)
|
|
106
|
+
result = await super()._async_devlog_executor(fn, *args, **kwargs)
|
|
107
|
+
self._do_logging(fn, result, *original_args, **original_kwargs)
|
|
108
|
+
return result
|
|
109
|
+
|
|
110
|
+
def _do_logging(self, fn: FunctionType, result: Any, *args: Any, **kwargs: Any) -> None:
|
|
111
|
+
logger = self.get_logger(fn)
|
|
112
|
+
|
|
113
|
+
extra = {self.result_format_variable: result, self.callable_format_variable: fn}
|
|
114
|
+
msg = self.build_msg(fn, fn_args=args, fn_kwargs=kwargs, **extra)
|
|
115
|
+
|
|
116
|
+
self.log(logger, self.log_level, msg)
|
|
117
|
+
if self.trace_stack:
|
|
118
|
+
stack = traceback.StackSummary(
|
|
119
|
+
LoggingDecorator.get_stack_summary(self.include_decorator, capture_locals=self.capture_locals)
|
|
120
|
+
)
|
|
121
|
+
self.log(logger, logging.DEBUG, "".join(stack.format()).strip())
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class LogOnError(LoggingDecorator):
|
|
125
|
+
r"""A logging decorator that logs the error of the function.
|
|
126
|
+
|
|
127
|
+
This decorator will log the error of the function using the logger and the handler
|
|
128
|
+
provided in the constructor.
|
|
129
|
+
|
|
130
|
+
Attributes:
|
|
131
|
+
log_level: The log level to use for logging.
|
|
132
|
+
message: The message format for the log.
|
|
133
|
+
logger: The logger to use for logging.
|
|
134
|
+
If not set, the logger will be created using the module name of the function.
|
|
135
|
+
handler: The handler to use for logging.
|
|
136
|
+
args_kwargs: If True, the message will accept {args} {kwargs} format.
|
|
137
|
+
trace_stack: Whether to include the stack trace in the log.
|
|
138
|
+
trace_stack_message:The message format for the stack trace log.
|
|
139
|
+
on_exception: The exception that will catch. Empty mean everything.
|
|
140
|
+
reraise: Whether to reraise the exception or supress it.
|
|
141
|
+
exception_format_variable: The variable to use for the error.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
def __init__(self, log_level: int = logging.ERROR,
|
|
145
|
+
message: Optional[str] = None,
|
|
146
|
+
on_exceptions: Optional[Union[Type[BaseException], Tuple[Type[BaseException], ...]]] = None,
|
|
147
|
+
reraise: bool = True, exception_format_variable: str = "error", **kwargs: Any):
|
|
148
|
+
super().__init__(log_level, message, **kwargs)
|
|
149
|
+
if message is None:
|
|
150
|
+
self.message = "Error in func {{{cal_var}.__name__}} " \
|
|
151
|
+
"with args {{args}}, kwargs {{kwargs}}\n{{{except_var}}}.".format(
|
|
152
|
+
cal_var=self.callable_format_variable,
|
|
153
|
+
except_var=exception_format_variable
|
|
154
|
+
)
|
|
155
|
+
self.on_exceptions: Union[Type[BaseException], Tuple[Type[BaseException], ...]] = on_exceptions if \
|
|
156
|
+
on_exceptions is not None else BaseException
|
|
157
|
+
self.reraise = reraise
|
|
158
|
+
self.exception_format_variable = exception_format_variable
|
|
159
|
+
self.capture_locals = self.trace_stack or self.capture_locals
|
|
160
|
+
|
|
161
|
+
def _devlog_executor(self, fn: FunctionType, *args: Any, **kwargs: Any) -> Any:
|
|
162
|
+
__tracebackhide__ = True
|
|
163
|
+
original_args, original_kwargs = args, kwargs
|
|
164
|
+
if self._has_sensitive(args, kwargs):
|
|
165
|
+
args, kwargs = self._unwrap_args(args, kwargs)
|
|
166
|
+
try:
|
|
167
|
+
return super()._devlog_executor(fn, *args, **kwargs)
|
|
168
|
+
except BaseException as e:
|
|
169
|
+
self._on_error(fn, e, *original_args, **original_kwargs)
|
|
170
|
+
|
|
171
|
+
async def _async_devlog_executor(self, fn: FunctionType, *args: Any, **kwargs: Any) -> Any:
|
|
172
|
+
__tracebackhide__ = True
|
|
173
|
+
original_args, original_kwargs = args, kwargs
|
|
174
|
+
if self._has_sensitive(args, kwargs):
|
|
175
|
+
args, kwargs = self._unwrap_args(args, kwargs)
|
|
176
|
+
try:
|
|
177
|
+
return await super()._async_devlog_executor(fn, *args, **kwargs)
|
|
178
|
+
except BaseException as e:
|
|
179
|
+
self._on_error(fn, e, *original_args, **original_kwargs)
|
|
180
|
+
|
|
181
|
+
def _do_logging(self, fn: FunctionType, *args: Any, **kwargs: Any) -> None:
|
|
182
|
+
logger = self.get_logger(fn)
|
|
183
|
+
full_traceback = traceback.TracebackException(*sys.exc_info(), capture_locals=self.capture_locals)
|
|
184
|
+
custom_traceback = list(LoggingDecorator.get_stack_summary(
|
|
185
|
+
self.include_decorator, capture_locals=self.capture_locals)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# exclude all the stack trace that are in this module
|
|
189
|
+
for frame in full_traceback.stack:
|
|
190
|
+
if not LoggingDecorator._is_internal_file(frame.filename) or self.include_decorator:
|
|
191
|
+
custom_traceback.append(frame)
|
|
192
|
+
|
|
193
|
+
full_traceback.stack = traceback.StackSummary(custom_traceback)
|
|
194
|
+
|
|
195
|
+
extra = {self.callable_format_variable: fn,
|
|
196
|
+
self.exception_format_variable: "".join(list(full_traceback.format(chain=True))).strip()}
|
|
197
|
+
|
|
198
|
+
msg = self.build_msg(fn, fn_args=args, fn_kwargs=kwargs, **extra)
|
|
199
|
+
|
|
200
|
+
self.log(logger, self.log_level, msg)
|
|
201
|
+
|
|
202
|
+
def _on_error(self, fn: FunctionType, exception: BaseException, *args: Any, **kwargs: Any) -> None:
|
|
203
|
+
__tracebackhide__ = True
|
|
204
|
+
if issubclass(exception.__class__, self.on_exceptions) and not hasattr(exception, "_devlog_logged"):
|
|
205
|
+
self._do_logging(fn, *args, **kwargs)
|
|
206
|
+
exception._devlog_logged = True
|
|
207
|
+
if self.reraise:
|
|
208
|
+
raise
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# Register this file as internal
|
|
212
|
+
LoggingDecorator._internal_files.add(__file__)
|
devlog/sanitize.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import operator
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Sensitive:
|
|
6
|
+
"""Transparent proxy that marks a value for redaction in devlog logs only.
|
|
7
|
+
|
|
8
|
+
The wrapped value passes through to functions unchanged — all attribute access,
|
|
9
|
+
operators, iteration, etc. are delegated to the real value. Redaction only
|
|
10
|
+
happens when devlog formats log messages.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
password = Sensitive("hunter2")
|
|
14
|
+
login(password) # function receives "hunter2" normally
|
|
15
|
+
# but devlog logs show: login(password='***')
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
__slots__ = ('_devlog_value', '_devlog_mask')
|
|
19
|
+
|
|
20
|
+
def __init__(self, value: Any, mask: str = "***"):
|
|
21
|
+
object.__setattr__(self, '_devlog_value', value)
|
|
22
|
+
object.__setattr__(self, '_devlog_mask', mask)
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def real_value(self) -> Any:
|
|
26
|
+
return object.__getattribute__(self, '_devlog_value')
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def mask(self) -> str:
|
|
30
|
+
return object.__getattribute__(self, '_devlog_mask')
|
|
31
|
+
|
|
32
|
+
# --- transparent proxy ---
|
|
33
|
+
def __getattr__(self, name: str) -> Any:
|
|
34
|
+
return getattr(object.__getattribute__(self, '_devlog_value'), name)
|
|
35
|
+
|
|
36
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
37
|
+
setattr(object.__getattribute__(self, '_devlog_value'), name, value)
|
|
38
|
+
|
|
39
|
+
def __delattr__(self, name: str) -> None:
|
|
40
|
+
delattr(object.__getattribute__(self, '_devlog_value'), name)
|
|
41
|
+
|
|
42
|
+
def __str__(self) -> str:
|
|
43
|
+
return str(object.__getattribute__(self, '_devlog_value'))
|
|
44
|
+
|
|
45
|
+
def __repr__(self) -> str:
|
|
46
|
+
return repr(object.__getattribute__(self, '_devlog_value'))
|
|
47
|
+
|
|
48
|
+
def __bool__(self) -> bool:
|
|
49
|
+
return bool(object.__getattribute__(self, '_devlog_value'))
|
|
50
|
+
|
|
51
|
+
def __len__(self) -> int:
|
|
52
|
+
return len(object.__getattribute__(self, '_devlog_value'))
|
|
53
|
+
|
|
54
|
+
def __iter__(self):
|
|
55
|
+
return iter(object.__getattribute__(self, '_devlog_value'))
|
|
56
|
+
|
|
57
|
+
def __contains__(self, item: Any) -> bool:
|
|
58
|
+
return item in object.__getattribute__(self, '_devlog_value')
|
|
59
|
+
|
|
60
|
+
def __getitem__(self, key: Any) -> Any:
|
|
61
|
+
return object.__getattribute__(self, '_devlog_value')[key]
|
|
62
|
+
|
|
63
|
+
def __setitem__(self, key: Any, value: Any) -> None:
|
|
64
|
+
object.__getattribute__(self, '_devlog_value')[key] = value
|
|
65
|
+
|
|
66
|
+
def __delitem__(self, key: Any) -> None:
|
|
67
|
+
del object.__getattribute__(self, '_devlog_value')[key]
|
|
68
|
+
|
|
69
|
+
def __eq__(self, other: Any) -> bool:
|
|
70
|
+
val = object.__getattribute__(self, '_devlog_value')
|
|
71
|
+
other_val = other.real_value if isinstance(other, Sensitive) else other
|
|
72
|
+
return val == other_val
|
|
73
|
+
|
|
74
|
+
def __hash__(self) -> int:
|
|
75
|
+
return hash(object.__getattribute__(self, '_devlog_value'))
|
|
76
|
+
|
|
77
|
+
def __lt__(self, other: Any) -> bool:
|
|
78
|
+
val = object.__getattribute__(self, '_devlog_value')
|
|
79
|
+
other_val = other.real_value if isinstance(other, Sensitive) else other
|
|
80
|
+
return val < other_val
|
|
81
|
+
|
|
82
|
+
def __le__(self, other: Any) -> bool:
|
|
83
|
+
val = object.__getattribute__(self, '_devlog_value')
|
|
84
|
+
other_val = other.real_value if isinstance(other, Sensitive) else other
|
|
85
|
+
return val <= other_val
|
|
86
|
+
|
|
87
|
+
def __gt__(self, other: Any) -> bool:
|
|
88
|
+
val = object.__getattribute__(self, '_devlog_value')
|
|
89
|
+
other_val = other.real_value if isinstance(other, Sensitive) else other
|
|
90
|
+
return val > other_val
|
|
91
|
+
|
|
92
|
+
def __ge__(self, other: Any) -> bool:
|
|
93
|
+
val = object.__getattribute__(self, '_devlog_value')
|
|
94
|
+
other_val = other.real_value if isinstance(other, Sensitive) else other
|
|
95
|
+
return val >= other_val
|
|
96
|
+
|
|
97
|
+
def __add__(self, other: Any) -> Any:
|
|
98
|
+
val = object.__getattribute__(self, '_devlog_value')
|
|
99
|
+
other_val = other.real_value if isinstance(other, Sensitive) else other
|
|
100
|
+
return val + other_val
|
|
101
|
+
|
|
102
|
+
def __radd__(self, other: Any) -> Any:
|
|
103
|
+
val = object.__getattribute__(self, '_devlog_value')
|
|
104
|
+
return other + val
|
|
105
|
+
|
|
106
|
+
def __mul__(self, other: Any) -> Any:
|
|
107
|
+
val = object.__getattribute__(self, '_devlog_value')
|
|
108
|
+
other_val = other.real_value if isinstance(other, Sensitive) else other
|
|
109
|
+
return val * other_val
|
|
110
|
+
|
|
111
|
+
def __rmul__(self, other: Any) -> Any:
|
|
112
|
+
val = object.__getattribute__(self, '_devlog_value')
|
|
113
|
+
return other * val
|
|
114
|
+
|
|
115
|
+
def __int__(self) -> int:
|
|
116
|
+
return int(object.__getattribute__(self, '_devlog_value'))
|
|
117
|
+
|
|
118
|
+
def __float__(self) -> float:
|
|
119
|
+
return float(object.__getattribute__(self, '_devlog_value'))
|
|
120
|
+
|
|
121
|
+
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
|
122
|
+
return object.__getattribute__(self, '_devlog_value')(*args, **kwargs)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def unwrap_sensitive(value: Any) -> Any:
|
|
126
|
+
"""Unwrap a Sensitive value to its real value, or return as-is."""
|
|
127
|
+
if isinstance(value, Sensitive):
|
|
128
|
+
return value.real_value
|
|
129
|
+
return value
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def format_value(value: Any, param_name: str = "",
|
|
133
|
+
sanitize_params: set = None) -> str:
|
|
134
|
+
"""Format a value for log display, applying redaction as needed."""
|
|
135
|
+
if isinstance(value, Sensitive):
|
|
136
|
+
return object.__getattribute__(value, '_devlog_mask')
|
|
137
|
+
if sanitize_params and param_name in sanitize_params:
|
|
138
|
+
return "***"
|
|
139
|
+
return repr(value)
|