From a0f46d606b9216198134075cf73793fef9f76d37 Mon Sep 17 00:00:00 2001 From: Perchun Pak Date: Wed, 18 Mar 2026 17:12:47 +0100 Subject: [PATCH 1/4] Add support for filtering out secrets when `diagnose=True` --- README.md | 33 ++++++++++++++++++- docs/resources/recipes.rst | 5 ++- loguru/_better_exceptions.py | 13 ++++++++ loguru/_defaults.py | 1 + loguru/_logger.py | 6 ++++ tests/exceptions/output/diagnose/excludes.txt | 13 ++++++++ tests/exceptions/source/diagnose/excludes.py | 25 ++++++++++++++ tests/test_exceptions_formatting.py | 1 + 8 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 tests/exceptions/output/diagnose/excludes.txt create mode 100644 tests/exceptions/source/diagnose/excludes.py diff --git a/README.md b/README.md index 470e1557..dfc87cea 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,8 @@ Logging exceptions that occur in your code is important to track bugs, but it's The code: ```python -# Caution, "diagnose=True" is the default and may leak sensitive data in prod +# Caution, "diagnose=True" is the default and may leak sensitive data in prod. +# Read further for the solution logger.add("out.log", backtrace=True, diagnose=True) def func(a, b): @@ -191,6 +192,36 @@ ZeroDivisionError: division by zero Note that this feature won't work on default Python REPL due to unavailable frame data. +But for passwords and other credentials, you should exclude them using `diagnose_excludes` parameter: + +```python +logger.add("out.log", backtrace=True, diagnose=True, diagnose_excludes=["myS3cr3tP@ss!"]) + +def connect_to_db(password): + # ... + raise TimeoutError("could not connect to the database") + +password = "myS3cr3tP@ss!" +connect_to_db(password) +``` + +This will replace all occurrences of `myS3cr3tP@ss!` with `` so the result traceback would be something like + +```none +2026-03-18 17:06:44.822 | ERROR | __main__::15 - What?! +Traceback (most recent call last): + +> File "test.py", line 13, in + connect_to_db(password) + │ └ '' + └ + + File "test.py", line 9, in connect_to_db + raise TimeoutError("could not connect to the database") + +TimeoutError: could not connect to the database +``` + See also: [Security considerations when using Loguru](https://loguru.readthedocs.io/en/stable/resources/recipes.html#security-considerations-when-using-loguru). ### Structured logging as needed diff --git a/docs/resources/recipes.rst b/docs/resources/recipes.rst index fa725f30..fd41a7ff 100644 --- a/docs/resources/recipes.rst +++ b/docs/resources/recipes.rst @@ -110,11 +110,14 @@ Another danger due to external input is the possibility of a log injection attac logger.info("User " + username + " logged in.") -Note that by default, Loguru will display the value of existing variables when an ``Exception`` is logged. This is very useful for debugging but could lead to credentials appearing in log files. Make sure to turn it off in production (or set the ``LOGURU_DIAGNOSE=NO`` environment variable). +Note that by default, Loguru will display the value of existing variables when an ``Exception`` is logged. This is very useful for debugging but could lead to credentials appearing in log files. Make sure to add your sensitive data to `diagnose_excludes` or turn it off in production (or set the ``LOGURU_DIAGNOSE=NO`` environment variable). .. code:: + logger.add("out.log", diagnose=True, diagnose_excludes=["myS3cr3tP@ss!"]) + # or disable diagnose using logger.add("out.log", diagnose=False) + # or set the ``LOGURU_DIAGNOSE=NO`` environment variable Another thing you should consider is to change the access permissions of your log file. Loguru creates files using the built-in |open| function, which means by default they might be read by a different user than the owner. If this is not desirable, be sure to modify the default access rights. diff --git a/loguru/_better_exceptions.py b/loguru/_better_exceptions.py index 8d88867e..2a4ce768 100644 --- a/loguru/_better_exceptions.py +++ b/loguru/_better_exceptions.py @@ -145,6 +145,7 @@ def __init__( colorize=False, backtrace=False, diagnose=True, + diagnose_excludes=None, theme=None, style=None, max_length=128, @@ -154,6 +155,11 @@ def __init__( ): self._colorize = colorize self._diagnose = diagnose + self._diagnose_excludes = ( + diagnose_excludes.split(",") + if isinstance(diagnose_excludes, str) and diagnose_excludes + else diagnose_excludes or [] + ) self._theme = theme or dict(self._default_theme) self._backtrace = backtrace self._syntax_highlighter = SyntaxHighlighter(style) @@ -345,6 +351,9 @@ def _format_value(self, v): except Exception: v = "" % type(v).__name__ + for exclude in self._diagnose_excludes: + v = v.replace(repr(exclude)[1:-1], "") + max_length = self._max_length if max_length is not None and len(v) > max_length: v = v[: max_length - 3] + "..." @@ -473,6 +482,10 @@ def _format_exception( # Remove final new line temporarily. error_message = exception_only[error_message_index][:-1] + for exclude in self._diagnose_excludes: + error_message = error_message.replace(repr(exclude)[1:-1], "") + error_message = error_message.replace(exclude, "") + if self._colorize: if ":" in error_message: exception_type, exception_value = error_message.split(":", 1) diff --git a/loguru/_defaults.py b/loguru/_defaults.py index 3ba5e12a..e080958f 100644 --- a/loguru/_defaults.py +++ b/loguru/_defaults.py @@ -42,6 +42,7 @@ def env(key, type_, default=None): LOGURU_SERIALIZE = env("LOGURU_SERIALIZE", bool, False) LOGURU_BACKTRACE = env("LOGURU_BACKTRACE", bool, True) LOGURU_DIAGNOSE = env("LOGURU_DIAGNOSE", bool, True) +LOGURU_DIAGNOSE_EXCLUDES = env("LOGURU_DIAGNOSE_EXCLUDES", str, "") LOGURU_ENQUEUE = env("LOGURU_ENQUEUE", bool, False) LOGURU_CONTEXT = env("LOGURU_CONTEXT", str, None) LOGURU_CATCH = env("LOGURU_CATCH", bool, True) diff --git a/loguru/_logger.py b/loguru/_logger.py index f0a36d8b..ac5890dc 100644 --- a/loguru/_logger.py +++ b/loguru/_logger.py @@ -269,6 +269,7 @@ def add( serialize=_defaults.LOGURU_SERIALIZE, backtrace=_defaults.LOGURU_BACKTRACE, diagnose=_defaults.LOGURU_DIAGNOSE, + diagnose_excludes=_defaults.LOGURU_DIAGNOSE_EXCLUDES, enqueue=_defaults.LOGURU_ENQUEUE, context=_defaults.LOGURU_CONTEXT, catch=_defaults.LOGURU_CATCH, @@ -302,6 +303,10 @@ def add( diagnose : |bool|, optional Whether the exception trace should display the variables values to ease the debugging. This should be set to ``False`` in production to avoid leaking sensitive data. + diagnose_excludes : |list| of |str|, optional + List of strings to exclude from variables in exceptions with ``diagnose=True``. + Use this if you would like to keep more context in production logs, + but don't want to leak credentials. enqueue : |bool|, optional Whether the messages to be logged should first pass through a multiprocessing-safe queue before reaching the sink. This is useful while logging to a file through multiple @@ -1023,6 +1028,7 @@ def add( colorize=colorize, encoding=encoding, diagnose=diagnose, + diagnose_excludes=diagnose_excludes, backtrace=backtrace, hidden_frames_filename=self.catch.__code__.co_filename, prefix=exception_prefix, diff --git a/tests/exceptions/output/diagnose/excludes.txt b/tests/exceptions/output/diagnose/excludes.txt new file mode 100644 index 00000000..ee2cabd7 --- /dev/null +++ b/tests/exceptions/output/diagnose/excludes.txt @@ -0,0 +1,13 @@ + +Traceback (most recent call last): + + File "tests/exceptions/source/diagnose/excludes.py", line 23, in  + connect_to_db(password) + │ └ '' + └  + + File "tests/exceptions/source/diagnose/excludes.py", line 18, in connect_to_db + raise TimeoutError("tried to connect to " + repr(connection_string)) +  └ 'foo bar baz' + +TimeoutError: tried to connect to 'foo bar baz' diff --git a/tests/exceptions/source/diagnose/excludes.py b/tests/exceptions/source/diagnose/excludes.py new file mode 100644 index 00000000..0b91ba4b --- /dev/null +++ b/tests/exceptions/source/diagnose/excludes.py @@ -0,0 +1,25 @@ +import sys + +from loguru import logger + +logger.remove() +logger.add( + sys.stderr, + format="", + colorize=True, + backtrace=False, + diagnose=True, + diagnose_excludes=["myS3cr\n3tP@ss!"], +) + + +def connect_to_db(password): + connection_string = "foo bar " + password + " baz" + raise TimeoutError("tried to connect to " + repr(connection_string)) + + +password = "myS3cr\n3tP@ss!" +try: + connect_to_db(password) +except TimeoutError: + logger.exception("") diff --git a/tests/test_exceptions_formatting.py b/tests/test_exceptions_formatting.py index c730b149..1aeb03be 100644 --- a/tests/test_exceptions_formatting.py +++ b/tests/test_exceptions_formatting.py @@ -166,6 +166,7 @@ def test_backtrace(filename): "attributes", "chained_both", "encoding", + "excludes", "global_variable", "indentation_error", "keyword_argument", From ae0dc63ed1f13529f0f467c5abad24a4ae86094a Mon Sep 17 00:00:00 2001 From: Perchun Pak Date: Wed, 18 Mar 2026 17:17:50 +0100 Subject: [PATCH 2/4] Add an entry to the changelog --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d6a74e15..64ba2bd3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,6 +15,7 @@ - Add requirement for ``TERM`` environment variable not to be ``"dumb"`` to enable colorization (`#1287 `_, thanks `@snosov1 `_). - Make ``logger.catch()`` usable as an asynchronous context manager (`#1084 `_). - Make ``logger.catch()`` compatible with asynchronous generators (`#1302 `_). +- Add new parameter ``diagnose_excludes`` to ``logger.add``, to allow excluding sensitive data from variables when ``diagnose=True`` (`#1447 `_). `0.7.3`_ (2024-12-06) ===================== From 678db16e65ab3991585a7782ea9dea59a4ede56f Mon Sep 17 00:00:00 2001 From: Perchun Pak Date: Wed, 18 Mar 2026 17:39:02 +0100 Subject: [PATCH 3/4] Add `diagnose_excludes` to the stubs --- loguru/__init__.pyi | 3 +++ 1 file changed, 3 insertions(+) diff --git a/loguru/__init__.pyi b/loguru/__init__.pyi index b15ac82e..8b4c9151 100644 --- a/loguru/__init__.pyi +++ b/loguru/__init__.pyi @@ -206,6 +206,7 @@ class Logger: serialize: bool = ..., backtrace: bool = ..., diagnose: bool = ..., + diagnose_excludes: list[str] = ..., enqueue: bool = ..., context: Optional[Union[str, BaseContext]] = ..., catch: bool = ... @@ -222,6 +223,7 @@ class Logger: serialize: bool = ..., backtrace: bool = ..., diagnose: bool = ..., + diagnose_excludes: list[str] = ..., enqueue: bool = ..., catch: bool = ..., context: Optional[Union[str, BaseContext]] = ..., @@ -239,6 +241,7 @@ class Logger: serialize: bool = ..., backtrace: bool = ..., diagnose: bool = ..., + diagnose_excludes: list[str] = ..., enqueue: bool = ..., context: Optional[Union[str, BaseContext]] = ..., catch: bool = ..., From 6b2a15007ec0c2429057b2232f9d0cfe977bc112 Mon Sep 17 00:00:00 2001 From: Perchun Pak Date: Wed, 18 Mar 2026 18:16:45 +0100 Subject: [PATCH 4/4] Fix tests --- tests/typesafety/test_logger.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/typesafety/test_logger.yml b/tests/typesafety/test_logger.yml index 939ae68c..7d8b729b 100644 --- a/tests/typesafety/test_logger.yml +++ b/tests/typesafety/test_logger.yml @@ -291,9 +291,9 @@ out: | main:2: error: No overload variant of "add" of "Logger" matches argument types "Callable[[Any], None]", "int" main:2: note: Possible overload variants: - main:2: note: def add(self, sink: Union[TextIO, Writable, Callable[[Message], None], Handler], *, level: Union[str, int] = ..., format: Union[str, Callable[[Record], str]] = ..., filter: Union[str, Callable[[Record], bool], dict[Optional[str], Union[str, int, bool]], None] = ..., colorize: Optional[bool] = ..., serialize: bool = ..., backtrace: bool = ..., diagnose: bool = ..., enqueue: bool = ..., context: Union[str, BaseContext, None] = ..., catch: bool = ...) -> int - main:2: note: def add(self, sink: Callable[[Message], Awaitable[None]], *, level: Union[str, int] = ..., format: Union[str, Callable[[Record], str]] = ..., filter: Union[str, Callable[[Record], bool], dict[Optional[str], Union[str, int, bool]], None] = ..., colorize: Optional[bool] = ..., serialize: bool = ..., backtrace: bool = ..., diagnose: bool = ..., enqueue: bool = ..., catch: bool = ..., context: Union[str, BaseContext, None] = ..., loop: Optional[AbstractEventLoop] = ...) -> int - main:2: note: def add(self, sink: Union[str, PathLike[str]], *, level: Union[str, int] = ..., format: Union[str, Callable[[Record], str]] = ..., filter: Union[str, Callable[[Record], bool], dict[Optional[str], Union[str, int, bool]], None] = ..., colorize: Optional[bool] = ..., serialize: bool = ..., backtrace: bool = ..., diagnose: bool = ..., enqueue: bool = ..., context: Union[str, BaseContext, None] = ..., catch: bool = ..., rotation: Union[str, int, time, timedelta, Callable[[Message, TextIO], bool], list[Union[str, int, time, timedelta, Callable[[Message, TextIO], bool]]], None] = ..., retention: Union[str, int, timedelta, Callable[[list[str]], None], None] = ..., compression: Union[str, Callable[[str], None], None] = ..., delay: bool = ..., watch: bool = ..., mode: str = ..., buffering: int = ..., encoding: str = ..., errors: Optional[str] = ..., newline: Optional[str] = ..., closefd: bool = ..., opener: Optional[Callable[[str, int], int]] = ...) -> int + main:2: note: def add(self, sink: Union[TextIO, Writable, Callable[[Message], None], Handler], *, level: Union[str, int] = ..., format: Union[str, Callable[[Record], str]] = ..., filter: Union[str, Callable[[Record], bool], dict[Optional[str], Union[str, int, bool]], None] = ..., colorize: Optional[bool] = ..., serialize: bool = ..., backtrace: bool = ..., diagnose: bool = ..., diagnose_excludes: list[str] = ..., enqueue: bool = ..., context: Union[str, BaseContext, None] = ..., catch: bool = ...) -> int + main:2: note: def add(self, sink: Callable[[Message], Awaitable[None]], *, level: Union[str, int] = ..., format: Union[str, Callable[[Record], str]] = ..., filter: Union[str, Callable[[Record], bool], dict[Optional[str], Union[str, int, bool]], None] = ..., colorize: Optional[bool] = ..., serialize: bool = ..., backtrace: bool = ..., diagnose: bool = ..., diagnose_excludes: list[str] = ..., enqueue: bool = ..., catch: bool = ..., context: Union[str, BaseContext, None] = ..., loop: Optional[AbstractEventLoop] = ...) -> int + main:2: note: def add(self, sink: Union[str, PathLike[str]], *, level: Union[str, int] = ..., format: Union[str, Callable[[Record], str]] = ..., filter: Union[str, Callable[[Record], bool], dict[Optional[str], Union[str, int, bool]], None] = ..., colorize: Optional[bool] = ..., serialize: bool = ..., backtrace: bool = ..., diagnose: bool = ..., diagnose_excludes: list[str] = ..., enqueue: bool = ..., context: Union[str, BaseContext, None] = ..., catch: bool = ..., rotation: Union[str, int, time, timedelta, Callable[[Message, TextIO], bool], list[Union[str, int, time, timedelta, Callable[[Message, TextIO], bool]]], None] = ..., retention: Union[str, int, timedelta, Callable[[list[str]], None], None] = ..., compression: Union[str, Callable[[str], None], None] = ..., delay: bool = ..., watch: bool = ..., mode: str = ..., buffering: int = ..., encoding: str = ..., errors: Optional[str] = ..., newline: Optional[str] = ..., closefd: bool = ..., opener: Optional[Callable[[str, int], int]] = ...) -> int - case: invalid_logged_object_formatting main: |