Skip to content

Commit 53b3243

Browse files
committed
Properly format log record & report logged extras to rollbar
This change passes the log record through the configured formatter, if any, and reports any non blocklisted logging record keys as extra data. This allows supporting Python logging's `extra` keyword argument which sets additional properties on the log record.
1 parent 7a74332 commit 53b3243

2 files changed

Lines changed: 74 additions & 31 deletions

File tree

rollbar/logger.py

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@
3232
_checkLevel = lambda lvl: lvl
3333

3434

35+
EXCLUDE_RECORD_KEYS = {
36+
# Attributes that are disallowed in `logging.Logger.makeRecord`
37+
'asctime',
38+
'message',
39+
# Attributes that are used internally by pyrollbar
40+
'extra_data',
41+
'payload_data',
42+
'request',
43+
*vars(logging.makeLogRecord({})).keys(),
44+
}
45+
46+
3547
def resolve_logging_types(obj):
3648
if isinstance(obj, (dict, ConvertingDict)):
3749
return {k: resolve_logging_types(v) for k, v in obj.items()}
@@ -116,9 +128,10 @@ def emit(self, record):
116128
'thread': record.thread,
117129
'threadName': record.threadName
118130
}
119-
}
120-
121-
extra_data.update(getattr(record, 'extra_data', {}))
131+
} | { # include any extras
132+
k: v for k, v in vars(record).items()
133+
if k not in EXCLUDE_RECORD_KEYS
134+
} | getattr(record, 'extra_data', {}) # include historical extra_data
122135

123136
payload_data = getattr(record, 'payload_data', {})
124137

@@ -133,18 +146,25 @@ def emit(self, record):
133146
# load the request
134147
request = getattr(record, "request", None) or rollbar.get_request()
135148

149+
# Rather than copy the log record and disable exception and stack trace
150+
# formatting, this does the same steps to prepare the log record
151+
# as `logging.Formatter.format` does before calling
152+
# `logging.Formatter.formatMessage`.
153+
formatter = self.formatter or logging._defaultFormatter
154+
record.message = record.getMessage()
155+
if formatter.usesTime():
156+
record.asctime = formatter.formatTime(record, formatter.datefmt)
157+
158+
message = formatter.formatMessage(record)
159+
136160
uuid = None
137161
try:
138162
# when not in an exception handler, exc_info == (None, None, None)
139163
if exc_info and exc_info[0]:
140164
if record.msg:
141165
message_template = {
142166
'body': {
143-
'trace': {
144-
'exception': {
145-
'description': record.getMessage()
146-
}
147-
}
167+
'trace': {'exception': {'description': message}}
148168
}
149169
}
150170
payload_data = rollbar.dict_merge(
@@ -156,7 +176,7 @@ def emit(self, record):
156176
extra_data=extra_data,
157177
payload_data=payload_data)
158178
else:
159-
uuid = rollbar.report_message(record.getMessage(),
179+
uuid = rollbar.report_message(message,
160180
level=level,
161181
request=request,
162182
extra_data=extra_data,

rollbar/test/test_loghandler.py

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,20 @@ class LogHandlerTest(BaseTest):
2626
def setUp(self):
2727
rollbar._initialized = False
2828
rollbar.SETTINGS = copy.deepcopy(_default_settings)
29-
30-
def _create_logger(self):
31-
logger = logging.getLogger(__name__)
32-
logger.setLevel(logging.DEBUG)
29+
self.logger = logging.getLogger(__name__)
30+
self.logger.setLevel(logging.DEBUG)
3331

3432
self.rollbar_handler = RollbarHandler(_test_access_token, _test_environment)
3533
self.rollbar_handler.setLevel(logging.WARNING)
3634

37-
logger.addHandler(self.rollbar_handler)
35+
self.logger.addHandler(self.rollbar_handler)
3836

39-
return logger
37+
def tearDown(self):
38+
self.logger.removeHandler(self.rollbar_handler)
4039

4140
@mock.patch('rollbar.send_payload')
4241
def test_message_gets_formatted(self, send_payload):
43-
logger = self._create_logger()
44-
logger.warning("Hello %d %s", 1, 'world')
42+
self.logger.warning("Hello %d %s", 1, 'world')
4543

4644
payload = send_payload.call_args[0][0]
4745

@@ -53,22 +51,20 @@ def test_message_gets_formatted(self, send_payload):
5351

5452
@mock.patch('rollbar.send_payload')
5553
def test_string_or_int_level(self, send_payload):
56-
logger = self._create_logger()
57-
logger.setLevel(logging.ERROR)
54+
self.logger.setLevel(logging.ERROR)
5855
self.rollbar_handler.setLevel('WARNING')
59-
logger.error("I am an error")
56+
self.logger.error("I am an error")
6057

6158
payload = send_payload.call_args[0][0]
6259

6360
self.assertEqual(payload['data']['level'], 'error')
6461

6562
self.rollbar_handler.setLevel(logging.WARNING)
66-
logger.error("I am an error")
63+
self.logger.error("I am an error")
6764

6865
self.assertEqual(payload['data']['level'], 'error')
6966

7067
def test_request_is_get_from_log_record_if_present(self):
71-
logger = self._create_logger()
7268
# Request objects vary depending on python frameworks or packages.
7369
# Using a dictionary for this test is enough.
7470
request = {"fake": "request", "for": "testing purporse"}
@@ -77,15 +73,15 @@ def test_request_is_get_from_log_record_if_present(self):
7773
# just need to be sure that proper rollbar function is called
7874
# with passed request as argument.
7975
with mock.patch("rollbar.report_message") as report_message_mock:
80-
logger.warning("Warning message", extra={"request": request})
76+
self.logger.warning("Warning message", extra={"request": request})
8177
self.assertEqual(report_message_mock.call_args[1]["request"], request)
8278

8379
# if you call logger.exception outside of an exception
8480
# handler, it shouldn't try to report exc_info, since it
8581
# won't have any
8682
with mock.patch("rollbar.report_exc_info") as report_exc_info:
8783
with mock.patch("rollbar.report_message") as report_message_mock:
88-
logger.exception("Exception message", extra={"request": request})
84+
self.logger.exception("Exception message", extra={"request": request})
8985
report_exc_info.assert_not_called()
9086
self.assertEqual(report_message_mock.call_args[1]["request"], request)
9187

@@ -94,14 +90,12 @@ def test_request_is_get_from_log_record_if_present(self):
9490
try:
9591
raise Exception()
9692
except:
97-
logger.exception("Exception message", extra={"request": request})
93+
self.logger.exception("Exception message", extra={"request": request})
9894
self.assertEqual(report_exc_info.call_args[1]["request"], request)
9995
report_message_mock.assert_not_called()
10096

10197
@mock.patch('rollbar.send_payload')
10298
def test_nested_exception_trace_chain(self, send_payload):
103-
logger = self._create_logger()
104-
10599
def _raise_context():
106100
bar_local = 'bar'
107101
raise CauseException('bar')
@@ -120,7 +114,7 @@ def _raise_ex():
120114
setattr(e, '__context__', context) # PEP-3134
121115
raise e
122116
except:
123-
logger.exception("Bad time")
117+
self.logger.exception("Bad time")
124118

125119
_raise_ex()
126120

@@ -139,8 +133,6 @@ def _raise_ex():
139133

140134
@mock.patch('rollbar.send_payload')
141135
def test_not_nested_exception_trace_chain(self, send_payload):
142-
logger = self._create_logger()
143-
144136
def _raise_context():
145137
bar_local = 'bar'
146138
raise CauseException('bar')
@@ -149,7 +141,7 @@ def _raise_ex():
149141
try:
150142
_raise_context()
151143
except:
152-
logger.exception("Bad time")
144+
self.logger.exception("Bad time")
153145

154146
_raise_ex()
155147

@@ -165,3 +157,34 @@ def _raise_ex():
165157
self.assertEqual('Bad time', payload['data']['custom']['exception']['description'])
166158
if trace is not None:
167159
self.assertEqual('Bad time', trace['exception']['description'])
160+
161+
@mock.patch('rollbar.send_payload')
162+
def test_logging_extra(self, send_payload):
163+
self.logger.error("Test error", extra=dict(test_attribute=1, test_other='test'))
164+
165+
payload = send_payload.call_args[0][0]
166+
self.assertEqual(payload['data']['body']['message'].get('test_attribute'), 1)
167+
self.assertEqual(payload['data']['body']['message'].get('test_other'), 'test')
168+
self.assertEqual(payload['data']['custom'].get('test_attribute'), 1)
169+
self.assertEqual(payload['data']['custom'].get('test_other'), 'test')
170+
171+
@mock.patch('rollbar.send_payload')
172+
def test_logging_extra_data(self, send_payload):
173+
self.logger.error(
174+
"Test error",
175+
extra=dict(extra_data=dict(test_attribute=1, test_other='test')))
176+
177+
payload = send_payload.call_args[0][0]
178+
self.assertEqual(payload['data']['body']['message'].get('test_attribute'), 1)
179+
self.assertEqual(payload['data']['body']['message'].get('test_other'), 'test')
180+
self.assertEqual(payload['data']['custom'].get('test_attribute'), 1)
181+
self.assertEqual(payload['data']['custom'].get('test_other'), 'test')
182+
183+
@mock.patch('rollbar.send_payload')
184+
def test_log_formatting(self, send_payload):
185+
self.rollbar_handler.formatter = logging.Formatter(
186+
'%(test_other)s[%(test_attribute)s]: %(message)s'
187+
)
188+
self.logger.error("Test error", extra=dict(test_attribute=1, test_other='test'))
189+
payload = send_payload.call_args[0][0]
190+
self.assertEqual(payload['data']['body']['message']['body'], 'test[1]: Test error')

0 commit comments

Comments
 (0)