Browse Source

Capture context in its own key for JSON-based formatters

The JSON formatter used to rely on services making their logging calls
and passing the context there. A call it expted would be

    LOG.debug("Some message", context=context)

This would end up in the "extra" section of the logging record. This is
not usually the case, as projects don't always pass the context on that
call. This also applies to the Fluent formatter which is based on the
JSON one.

For the JSON formatter, we already are getting the context from the
record. So lets use that if no context was provided in the record's
'extra' section. Finally, this places the context in its own section,
which is named 'context'.

Closes-Bug: #1730329
Depends-On: I2b245c1665c3587be3c476b803122788d186e5d5
Change-Id: I765dae17d2ecadce1672f16e432e748d5233acf8
tags/3.34.0^2
Juan Antonio Osorio Robles 1 year ago
parent
commit
1b012d0fc6

+ 22
- 10
oslo_log/formatters.py View File

@@ -221,12 +221,18 @@ class JSONFormatter(logging.Formatter):
221 221
         for key in getattr(record, 'extra_keys', []):
222 222
             if key not in extra:
223 223
                 extra[key] = getattr(record, key)
224
-        # If we saved a context object, explode it into the extra
225
-        # dictionary because the values are more useful than the
224
+        # The context object might have been given from the logging call. if
225
+        # that was the case, it'll come in the 'extra' entry already. If not,
226
+        # lets use the context we fetched above. In either case, we explode it
227
+        # into the 'context' entry because the values are more useful than the
226 228
         # object reference.
227
-        if 'context' in extra:
228
-            extra.update(_dictify_context(context))
229
-            del extra['context']
229
+        if 'context' in extra and extra['context']:
230
+            message['context'] = _dictify_context(extra['context'])
231
+        elif context:
232
+            message['context'] = _dictify_context(context)
233
+        else:
234
+            message['context'] = {}
235
+        extra.pop('context', None)
230 236
         message['extra'] = extra
231 237
 
232 238
         if record.exc_info:
@@ -290,12 +296,18 @@ class FluentFormatter(logging.Formatter):
290 296
         for key in getattr(record, 'extra_keys', []):
291 297
             if key not in extra:
292 298
                 extra[key] = getattr(record, key)
293
-        # If we saved a context object, explode it into the extra
294
-        # dictionary because the values are more useful than the
299
+        # The context object might have been given from the logging call. if
300
+        # that was the case, it'll come in the 'extra' entry already. If not,
301
+        # lets use the context we fetched above. In either case, we explode it
302
+        # into the extra dictionary because the values are more useful than the
295 303
         # object reference.
296
-        if 'context' in extra:
297
-            extra.update(_dictify_context(context))
298
-            del extra['context']
304
+        if 'context' in extra and extra['context']:
305
+            message['context'] = _dictify_context(extra['context'])
306
+        elif context:
307
+            message['context'] = _dictify_context(context)
308
+        else:
309
+            message['context'] = {}
310
+        extra.pop('context', None)
299 311
         message['extra'] = extra
300 312
 
301 313
         if record.exc_info:

+ 25
- 4
oslo_log/tests/unit/test_log.py View File

@@ -415,18 +415,38 @@ class JSONFormatterTestCase(LogTestBase):
415 415
                                        formatter=formatters.JSONFormatter)
416 416
         self._set_log_level_with_cleanup(self.log, logging.DEBUG)
417 417
 
418
-    def test_json(self):
418
+    def test_json_w_context_in_extras(self):
419 419
         test_msg = 'This is a %(test)s line'
420 420
         test_data = {'test': 'log'}
421 421
         local_context = _fake_context()
422 422
         self.log.debug(test_msg, test_data, key='value', context=local_context)
423
+        self._validate_json_data('test_json_w_context_in_extras', test_msg,
424
+                                 test_data, local_context)
423 425
 
426
+    def test_json_w_fetched_global_context(self):
427
+        test_msg = 'This is a %(test)s line'
428
+        test_data = {'test': 'log'}
429
+        local_context = _fake_context()
430
+        # NOTE we're not passing the context explicitly here. But it'll add the
431
+        # context to the extras anyway since the call to fake_context adds the
432
+        # context to the thread. The context will be fetched with the
433
+        # _update_record_with_context call that's done in the formatter.
434
+        self.log.debug(test_msg, test_data, key='value')
435
+        self._validate_json_data('test_json_w_fetched_global_context',
436
+                                 test_msg, test_data, local_context)
437
+
438
+    def _validate_json_data(self, testname, test_msg, test_data, ctx):
424 439
         data = jsonutils.loads(self.stream.getvalue())
425 440
         self.assertTrue(data)
426 441
         self.assertIn('extra', data)
442
+        self.assertIn('context', data)
427 443
         extra = data['extra']
444
+        context = data['context']
445
+        self.assertNotIn('context', extra)
428 446
         self.assertEqual('value', extra['key'])
429
-        self.assertEqual(local_context.user, extra['user'])
447
+        self.assertEqual(ctx.user, context['user'])
448
+        self.assertEqual(ctx.user_name, context['user_name'])
449
+        self.assertEqual(ctx.project_name, context['project_name'])
430 450
         self.assertEqual('test-json', data['name'])
431 451
 
432 452
         self.assertEqual(test_msg % test_data, data['message'])
@@ -434,7 +454,7 @@ class JSONFormatterTestCase(LogTestBase):
434 454
         self.assertEqual(test_data, data['args'])
435 455
 
436 456
         self.assertEqual('test_log.py', data['filename'])
437
-        self.assertEqual('test_json', data['funcname'])
457
+        self.assertEqual(testname, data['funcname'])
438 458
 
439 459
         self.assertEqual('DEBUG', data['levelname'])
440 460
         self.assertEqual(logging.DEBUG, data['levelno'])
@@ -563,8 +583,9 @@ class FluentFormatterTestCase(LogTestBase):
563 583
         self.assertIn('lineno', data)
564 584
         self.assertIn('extra', data)
565 585
         extra = data['extra']
586
+        context = data['context']
566 587
         self.assertEqual('value', extra['key'])
567
-        self.assertEqual(local_context.user, extra['user'])
588
+        self.assertEqual(local_context.user, context['user'])
568 589
         self.assertEqual('test-fluent', data['name'])
569 590
 
570 591
         self.assertEqual(test_msg % test_data, data['message'])

+ 6
- 0
releasenotes/notes/add-context-section-0b2f411ec64f42f6.yaml View File

@@ -0,0 +1,6 @@
1
+---
2
+features:
3
+  - |
4
+    The JSON based formatters (namely JSONFormatter and FluentFormatter) now
5
+    output an extra section called 'context' that contains the context-related
6
+    keys and values, e.g. user, project and domain.

Loading…
Cancel
Save