Skip to content

Commit 13d8f1c

Browse files
committed
Add exc_info support
1 parent 2eaf5ca commit 13d8f1c

File tree

2 files changed

+73
-7
lines changed

2 files changed

+73
-7
lines changed

logfmter/formatter.py

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1+
import io
12
import logging
23
import numbers
3-
from typing import Tuple
4+
import traceback
5+
from contextlib import closing
6+
from types import TracebackType
7+
from typing import Tuple, Type, cast
8+
9+
ExcInfo = Tuple[Type[BaseException], BaseException, TracebackType]
410

511
# Reserved log record attributes cannot be overwritten. They
612
# will not included in the formatted log.
@@ -67,6 +73,31 @@ def format_value(cls, value) -> str:
6773

6874
return cls.format_string(str(value))
6975

76+
@classmethod
77+
def format_exc_info(cls, exc_info: ExcInfo) -> str:
78+
"""
79+
Format the provided exc_info into a logfmt formatted string.
80+
81+
This function should only be used to format exceptions which are
82+
currently being handled. Not with those exceptions which are
83+
manually passed into the logger. For example:
84+
85+
try:
86+
raise Exception()
87+
except Exception:
88+
logging.exception()
89+
"""
90+
_type, exc, tb = exc_info
91+
92+
with closing(io.StringIO()) as sio:
93+
traceback.print_exception(_type, exc, tb, None, sio)
94+
value = sio.getvalue()
95+
96+
# Tracebacks have a single trailing newline that we don't need.
97+
value = value.rstrip("\n")
98+
99+
return cls.format_string(value)
100+
70101
@classmethod
71102
def format_params(cls, params: dict) -> str:
72103
"""
@@ -95,9 +126,12 @@ def format(self, record: logging.LogRecord) -> str:
95126
extra = self.get_extra(record)
96127
params = {"msg": record.getMessage(), **extra}
97128

98-
return " ".join(
99-
(
100-
"at={}".format(record.levelname),
101-
self.format_params(params),
102-
)
103-
)
129+
tokens = ["at={}".format(record.levelname), self.format_params(params)]
130+
131+
if record.exc_info:
132+
# Cast exc_info to its not null variant to make mypy happy.
133+
exc_info = cast(ExcInfo, record.exc_info)
134+
135+
tokens.append("exc_info={}".format(self.format_exc_info(exc_info)))
136+
137+
return " ".join(tokens)

tests/test_formatter.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import sys
23

34
import pytest
45

@@ -49,6 +50,23 @@ def test_format_value(value, expected):
4950
assert Logfmter.format_value(value) == expected
5051

5152

53+
def test_format_exc_info():
54+
try:
55+
raise Exception("alpha")
56+
except Exception:
57+
exc_info = sys.exc_info()
58+
59+
value = Logfmter().format_exc_info(exc_info)
60+
61+
assert value.startswith('"') and value.endswith('"')
62+
63+
tokens = value.strip('"').split("\\n")
64+
65+
assert len(tokens) == 4
66+
assert "Traceback (most recent call last):" in tokens
67+
assert "Exception: alpha" in tokens
68+
69+
5270
@pytest.mark.parametrize(
5371
"value,expected",
5472
[({"a": 1}, "a=1"), ({"a": 1, "b": 2}, "a=1 b=2"), ({"a": " "}, 'a=" "')],
@@ -79,6 +97,20 @@ def test_get_extra(record, expected):
7997
({"levelname": "INFO", "msg": "test", "a": 1}, "at=INFO msg=test a=1"),
8098
# All parameter values will be passed through the format pipeline.
8199
({"levelname": "INFO", "msg": "="}, 'at=INFO msg="="'),
100+
# Any existing exc_info will be appropriately formatted and
101+
# added to the log output.
102+
(
103+
{
104+
"levelname": "INFO",
105+
"msg": "alpha",
106+
"exc_info": (
107+
Exception,
108+
Exception("alpha"),
109+
None,
110+
), # We don't pass a traceback, because they are difficult to fake.
111+
},
112+
'at=INFO msg=alpha exc_info="Exception: alpha"',
113+
),
82114
],
83115
)
84116
def test_format(record, expected):

0 commit comments

Comments
 (0)