Warning
Most of the documentation was written prior to version 0.5 and needs to be updated. This work has now started for version 0.7 and we aim to have it completed before version 0.8 is available.
Logging and keeping confidential information secret¶
In some situations, it is useful to log exceptions that are raised and continue the execution. Since the information cannot be queried interactively, one might want to capture all the required information to figure out what went wrong. While friendly_traceback was not initially designed with this purpose in mind, it is perfectly suitable to use in such cases.
Let’s look at a fairly detailed example meant for more advanced programmes. First, here’s the code that we wish to run
1def compute_fraction(numerator, denominator):
2 return numerator / denominator
3
4a = 4
5b = 2
6c = 1
7# The following will raise an exception
8
9def fn_1(a, b):
10 d, e = 4, 5
11 irrelevant = d * e
12 return irrelevant + fn_2(c, a - b**2) + irrelevant
13
14def fn_2(secret_x, secret_y):
15 return fn_3 (secret_x, secret_y)
16
17def fn_3(x, y):
18 return compute_fraction(x, y)
19
20
21fraction = fn_1(a, b)
At this point, note on line 12 that the fn_2
function call is
part of an expression between two terms that we named irrelevant
.
This will be relevant later.
Furthermore, on line 15, we have two variable names beginning with secret
.
How to use logging: an example¶
Suppose we wish to run this code and log any exception that arise.
Furthermore, we do not want to reveal what the value of any variable
whose name begins by secret
, while having as much information as possible
about the state of the program and values of variables when
the exception was raised. Here’s how we can do it.
1import friendly_traceback
2from friendly_traceback.console_helpers import history, where
3import logging
4
5# Configure as desired before running the code
6logging.basicConfig(filename="ignore.log")
7friendly_traceback.set_stream(redirect="capture")
8
9# We don't ever want to show the name
10friendly_traceback.hide_secrets(patterns=[r"secret\w*"])
11
12try:
13 import example_logging_source
14except Exception:
15 friendly_traceback.explain_traceback()
16 log = friendly_traceback.get_output()
17
18 # The following will add some detailed information about variables
19 # in each frame, in case it is relevant
20 where(more=True)
21 where_log = friendly_traceback.get_output()
22
23 history.clear() # prevent memory leaks
24
25 full_log = (
26 " Output from friendly_traceback\n"
27 + log
28 + "\n------More details--------\n"
29 + where_log
30 + "\n======Original traceback=======\n"
31 )
32
33 # exc_info=True below will append the original traceback
34 logging.error(full_log, exc_info=True)
On line 7 we set friendly_traceback so that its output is (temporarily) captured rather than printed out.
On line xxx we add a regular expression pattern identifying variable names whose value we wish to keep secret.
On line 10, we extract the content that was captured and, in doing so, clear the buffer where that content was held. It is possible to pass the argument
flush=False
to keep that content if needed.By default, friendly_traceback gives a “short” traceback, with more details given for the initial and final frame. On line 20, with
where(more=True)
, we get friendly_traceback to output detailed information about each frame involved in the traceback.By default, friendly_traceback keeps a copy of each traceback that was generated. This is sometimes useful in interactive mode, especially if warnings are emitted as they do not cause an interruption to the program. By calling
history.clear()
on line 23, we remove this content, helping to reduce the risk of memory leaks.
Analysing the output¶
The example described previously yields the following result, to which we add comments afterwords.
1ERROR:root: Output from friendly_traceback
2
3Traceback (most recent call last):
4 File "HOME:\example_logging.py", line 13, in <module>
5 import example_logging_source
6 ... More lines not shown. ...
7 File "HOME:\example_logging_source.py", line 18, in fn_3
8 return compute_fraction(x, y)
9 File "HOME:\example_logging_source.py", line 2, in compute_fraction
10 return numerator / denominator
11ZeroDivisionError: division by zero
12
13 A `ZeroDivisionError` occurs when you are attempting to divide a value
14 by zero either directly or by using some other mathematical operation.
15
16 You are dividing by the following term
17
18 denominator
19
20 which is equal to zero.
21
22 Execution stopped on line `13` of file 'HOME:\example_logging.py'.
23
24 9| # We don't ever want to show the name
25 10| friendly_traceback.hide_secrets(patterns=[r"secret\w*"])
26 11|
27 12| try:
28 -->13| import example_logging_source
29 14| except Exception:
30
31 Exception raised on line `2` of file 'HOME:\example_logging_source.py'.
32
33 1| def compute_fraction(numerator, denominator):
34 -->2| return numerator / denominator
35 ^^^^^^^^^^^^^^^^^^^^^^^
36
37 denominator: 0
38 numerator: 1
39
40
41------More details--------
42
43 File 'HOME:\example_logging.py', line `13`
44 9| # We don't ever want to show the name
45 10| friendly_traceback.hide_secrets(patterns=[r"secret\w*"])
46 11|
47 12| try:
48 -->13| import example_logging_source
49 14| except Exception:
50
51
52 File 'HOME:\example_logging_source.py', line `21`
53 15| return fn_3 (secret_x, secret_y)
54 16|
55 17| def fn_3(x, y):
56 18| return compute_fraction(x, y)
57 :
58 -->21| fraction = fn_1(a, b)
59 ^^^^^^^^^^
60
61 a: 4
62 b: 2
63 fn_1: <function fn_1>
64
65 File 'HOME:\example_logging_source.py', line `12`
66 9| def fn_1(a, b):
67 10| d, e = 4, 5
68 11| irrelevant = d * e
69 -->12| return irrelevant + fn_2(c, a - b**2) + irrelevant
70 ^^^^^^^^^^^^^^^^^
71
72 a: 4
73 b: 2
74 global c: 1
75 global fn_2: <function fn_2>
76 a - b**2: 0
77 b**2: 4
78
79 File 'HOME:\example_logging_source.py', line `15`
80 14| def fn_2(secret_x, secret_y):
81 -->15| return fn_3 (secret_x, secret_y)
82 ^^^^^^^^^^^^^^^^^^^^^^^^^
83
84 secret_x: '••••••'
85 secret_y: '••••••'
86 global fn_3: <function fn_3>
87
88 File 'HOME:\example_logging_source.py', line `18`
89 17| def fn_3(x, y):
90 -->18| return compute_fraction(x, y)
91 ^^^^^^^^^^^^^^^^^^^^^^
92
93 x: 1
94 y: 0
95 global compute_fraction: <function compute_fraction>
96
97 File 'HOME:\example_logging_source.py', line `2`
98 1| def compute_fraction(numerator, denominator):
99 -->2| return numerator / denominator
100 ^^^^^^^^^^^^^^^^^^^^^^^
101
102 denominator: 0
103 numerator: 1
104
105
106======Original traceback=======
107Traceback (most recent call last):
108 File "C:\Users\Andre\example_logging.py", line 13, in <module>
109 import example_logging_source
110 File "C:\Users\Andre\example_logging_source.py", line 21, in <module>
111 fraction = fn_1(a, b)
112 File "C:\Users\Andre\example_logging_source.py", line 12, in fn_1
113 return irrelevant + fn_2(c, a - b**2) + irrelevant
114 File "C:\Users\Andre\example_logging_source.py", line 15, in fn_2
115 return fn_3 (secret_x, secret_y)
116 File "C:\Users\Andre\example_logging_source.py", line 18, in fn_3
117 return compute_fraction(x, y)
118 File "C:\Users\Andre\example_logging_source.py", line 2, in compute_fraction
119 return numerator / denominator
120ZeroDivisionError: division by zero
friendly-traceback uses Alex Hall’s executing to narrow down the parts of the code in a traceback to those that are involved in generating an exception. For example, look at line 69 above where the part highlighted by carets (
^
) does not include the variable nameirrelevant
. Lines 72 to 79 shows the value of various relevant objects in this highlighted part. Please see the note at the bottom of this page which includes two questions.While cPython 3.11 does include such highlighting of relevant code regions in traceback, it only shows one line per frame. By contrast, Friendly will show relevant parts of a statement even it if spans multiple lines. Furthermore, it does so for Python version 3.6 and greater.
Lines 84 and 85 shows that two variables,
secret_1
andsecret_2
, were involved in the code included in the traceback. However, since their names are included by the pattern we declared as being that of confidential variables, their values are redacted.
About keeping secrets¶
Friendly gives you the possibility of testing your pattern deemed to be
confidential before you deploy your code. To protect against the possibility
of having some confidential information shown in the “value” of a given
variable, for example, if you have a dict containing keys whose values
should be confidential, Friendly will also see if the pattern
appears in the “value” (technically, the repr
) of the variable
and will redact it if it does. Furthermore, in order to reduce the length
of the display, the “value” will be truncated if it exceeds a certain length.
Here’s a concrete example:
import friendly_traceback as ft
ft.hide_secrets(["secret\w*"])
not_secret = "should be public"
secret1 = "confidential because of the variable name."
hidden = "contains a secret."
long_list = list(range(100))
def fn(): ...
for name, true_repr in [
("not_secret", not_secret),
("secret1", secret1),
("hidden", hidden),
("long_list", long_list),
("fn", fn)
]:
print(f"{name}: {true_repr=}")
result = ft.test_secrets(name)
print("display by friendly_traceback:\n", result, "\n")
And here’s the corresponding result:
not_secret: true_repr='should be public'
display by friendly_traceback:
not_secret: 'should be public'
secret1: true_repr='confidential because of the variable name.'
display by friendly_traceback:
secret1: '••••••'
hidden: true_repr='contains a secret.'
display by friendly_traceback:
hidden: '••••••'
long_list: true_repr=[0, 1, 2, 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, 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]
display by friendly_traceback:
long_list: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, ...]
len(long_list): 100
fn: true_repr=<function fn at 0x0000026D56C5F160>
display by friendly_traceback:
fn: <function fn>
Note
For those that use logging, would it be useful to have the option to show the value all the variables involved in a given frame, like what is done by
rich.traceback
and other “traceback enhancers”?As mentioned, the “value” (
repr
) shown is truncated if it exceeds a certain length. Would it be useful for this length to be adjustable?
If you have any opinion on any or both of these two questions, please feel free to open up an issue for discussion.