Skip to content

Commit 6143af1

Browse files
authored
Merge pull request #63 from geoadmin/feat-pb-931-python-exception-logging
PB-931 Change behavior of exc_text and stack_info - #major
2 parents 460eccf + 67e0651 commit 6143af1

File tree

3 files changed

+127
-15
lines changed

3 files changed

+127
-15
lines changed

README.md

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ All features can be fully configured from the configuration file.
1717

1818
**NOTE:** only python 3 is supported
1919

20-
:warning: **Version 3.x.x BREAKING CHANGES** see [Breaking Changes](#version-3xx-breaking-changes)
20+
:warning: **Version 5.x.x BREAKING CHANGES** see [Breaking Changes](#version-5xx-breaking-changes)
2121

2222
## Table of content
2323

@@ -67,7 +67,9 @@ All features can be fully configured from the configuration file.
6767
- [Case 7. Add all Log Extra as Dictionary to the Standard Formatter (including Django log extra)](#case-7-add-all-log-extra-as-dictionary-to-the-standard-formatter-including-django-log-extra)
6868
- [Case 8. Add Specific Log Extra to the Standard Formatter](#case-8-add-specific-log-extra-to-the-standard-formatter)
6969
- [Case 9. Django add request info to all log records](#case-9-django-add-request-info-to-all-log-records)
70+
- [Case 10. Add stack traces to log records](#case-10-add-stack-traces-to-log-records)
7071
- [Breaking Changes](#breaking-changes)
72+
- [Version 5.x.x Breaking Changes](#version-5xx-breaking-changes)
7173
- [Version 4.x.x Breaking Changes](#version-4xx-breaking-changes)
7274
- [Version 3.x.x Breaking Changes](#version-3xx-breaking-changes)
7375
- [Version 2.x.x Breaking Changes](#version-2xx-breaking-changes)
@@ -273,7 +275,7 @@ For more information on Pyramid Tweens see [Registering Tween](https://docs.pylo
273275

274276
## JSON Formatter
275277

276-
**JsonFormatter** is a python logging formatter that transform the log output into a json object.
278+
**JsonFormatter** is a python logging formatter that transforms the log output into a json object.
277279

278280
JSON log format is quite useful especially when the logs are sent to **LogStash**.
279281

@@ -1368,8 +1370,46 @@ handlers:
13681370
- request_fields
13691371
```
13701372
1373+
### Case 10. Add stack traces to log records
1374+
1375+
If you want to embed the stack trace of either an Exception or a log entry in general, you can do so with following additions to the logging call:
1376+
1377+
```python
1378+
import sys
1379+
1380+
logger.debug('My log with stack info', stack_info=True)
1381+
logger.critical('Exception happened', exc_info=sys.exc_info())
1382+
```
1383+
1384+
Or you simply call `logger.exception('Your message')` which automatically adds the exc_info. However this one is logging as in level `ERROR` and not `CRITICAL`.
1385+
1386+
This will make the stack info available for the formatter. It can be used for instance like follows:
1387+
1388+
```yaml
1389+
[...]
1390+
formatters:
1391+
json:
1392+
(): logging_utilities.formatters.json_formatter.JsonFormatter
1393+
fmt:
1394+
error:
1395+
stack_trace: exc_text
1396+
stack_info: stack_info
1397+
time: asctime
1398+
level: levelname
1399+
logger: name
1400+
module: module
1401+
message: message
1402+
request:
1403+
path: request.path
1404+
method: request.method
1405+
```
1406+
13711407
## Breaking Changes
13721408
1409+
### Version 5.x.x Breaking Changes
1410+
1411+
Previously, the fields `exc_text` and `stack_info` were always added to the log message if they existed. This behavior is slightly changed: instead of adding them to the message in any case, they are added to the record so that they can be configured via the `fmt` field in the logging configuration as described in [#case-10-add-stack-traces-to-log-records].
1412+
13731413
### Version 4.x.x Breaking Changes
13741414

13751415
From version 3.x.x to version 4.x.x there is the following breaking change:

logging_utilities/formatters/json_formatter.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -343,21 +343,19 @@ def format(self, record):
343343
if self.add_always_extra:
344344
extra = self._get_extra_attrs(record)
345345

346-
message = self.formatMessage(record)
347-
348-
if self.add_always_extra:
349-
self._add_extra_to_message(extra, message)
350-
351346
if record.exc_info:
352347
# Cache the traceback text to avoid converting it multiple times
353348
# (it's constant anyway)
354349
if not record.exc_text:
355350
record.exc_text = self.formatException(record.exc_info)
356-
if record.exc_text:
357-
message['exc_text'] = record.exc_text
358351

359352
if record.stack_info:
360-
message['stack_info'] = self.formatStack(record.stack_info)
353+
record.stack_info = self.formatStack(record.stack_info)
354+
355+
message = self.formatMessage(record)
356+
357+
if self.add_always_extra:
358+
self._add_extra_to_message(extra, message)
361359

362360
# When adding all extras, to avoid crash when a log message adds an extra with a non
363361
# serializable object, we add a default serializer.

tests/test_json_formatter.py

Lines changed: 79 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
dictionary = OrderedDict
1919

2020

21+
# pylint: disable=too-many-public-methods
2122
class BasicJsonFormatterTest(unittest.TestCase):
2223
maxDiff = None
2324

@@ -378,11 +379,8 @@ def test_stack_info(self):
378379
logger = logging.getLogger('test_formatter')
379380
self._configure_logger(
380381
logger,
381-
fmt=dictionary([
382-
('levelname', 'levelname'),
383-
('name', 'name'),
384-
('message', 'message'),
385-
])
382+
fmt=dictionary([('levelname', 'levelname'), ('name', 'name'),
383+
('message', 'message'), ('stack_info', 'stack_info')])
386384
)
387385
logger.info('Message with stack info', stack_info=True)
388386
message = json.loads(ctx.output[0], object_pairs_hook=dictionary)
@@ -787,3 +785,79 @@ def test_missing_attribute(self):
787785
("message", "Composed message without extra"),
788786
])
789787
)
788+
789+
def test_exc_info_without_configuration(self):
790+
"""
791+
Test that the exc_text doesn't appear if it's not configured
792+
in the logger, even though the logging call records it
793+
"""
794+
with self.assertLogs('test_formatter', level=logging.DEBUG) as ctx:
795+
logger = logging.getLogger('test_formatter')
796+
self._configure_logger(
797+
logger,
798+
fmt=dictionary([
799+
('level', 'levelname'),
800+
('request', ['request.path', 'request.method']),
801+
('message', 'message'),
802+
]),
803+
remove_empty=True,
804+
ignore_missing=True
805+
)
806+
807+
try:
808+
raise Exception("Error occurred")
809+
except Exception as err: # pylint: disable=broad-except
810+
logger.critical(
811+
str(err),
812+
exc_info=sys.exc_info(),
813+
extra={'request': {
814+
'path': '/my/path', 'method': 'GET', 'scheme': 'https'
815+
}}
816+
)
817+
818+
self.assertDictEqual(
819+
json.loads(ctx.output[0], object_pairs_hook=dictionary),
820+
dictionary([
821+
("level", "CRITICAL"),
822+
("request", ["/my/path", "GET"]),
823+
("message", "Error occurred"), # no exc_text because it wasn't configured
824+
])
825+
)
826+
827+
def test_stack_info_without_configuration(self):
828+
"""
829+
Test that the stack_info doesn't appear in the record when it's
830+
not configured, even though the log call records it
831+
"""
832+
with self.assertLogs('test_formatter', level=logging.DEBUG) as ctx:
833+
logger = logging.getLogger('test_formatter')
834+
self._configure_logger(
835+
logger,
836+
fmt=dictionary([
837+
('level', 'levelname'),
838+
('request', ['request.path', 'request.method']),
839+
('message', 'message'),
840+
]),
841+
remove_empty=True,
842+
ignore_missing=True
843+
)
844+
845+
logger.info(
846+
"Stack isn't wished for here",
847+
exc_info=sys.exc_info(),
848+
extra={'request': {
849+
'path': '/my/path', 'method': 'GET', 'scheme': 'https'
850+
}},
851+
# record the stack_info
852+
stack_info=True
853+
)
854+
855+
self.assertDictEqual(
856+
json.loads(ctx.output[0], object_pairs_hook=dictionary),
857+
dictionary([
858+
("level", "INFO"),
859+
("request", ["/my/path", "GET"]),
860+
("message",
861+
"Stack isn't wished for here"), # no stack_info because it wasn't configured
862+
])
863+
)

0 commit comments

Comments
 (0)