Add JSON log format to logging configuration (#2982)

Now patroni can be configured as bellow to log in json format.

```yaml
log:
  type: json
  format:
    - asctime: '@timestamp'
    - levelname: level
    - message
    - module
    - name: logger_name
  static_fields:
    app: patroni
```

This config produce this log:

```json
{
  "@timestamp": "2023-12-14 19:51:24,872",
  "level": "INFO",
  "message": "Lock owner: None; I am postgresql1",
  "module": "ha",
  "app": "patroni",
  "logger_name": "patroni.ha"
}
```
This commit is contained in:
علی سالمی
2024-01-16 10:42:48 +01:00
committed by GitHub
parent 266cdc4810
commit 5c4ee30dae
13 changed files with 493 additions and 26 deletions
+195
View File
@@ -3,6 +3,8 @@ import os
import sys
import unittest
import yaml
import json
from io import StringIO
from mock import Mock, patch
from patroni.config import Config
@@ -72,3 +74,196 @@ class TestPatroniLogger(unittest.TestCase):
_LOG.info('blabla')
logger.shutdown()
self.assertEqual(logger.records_lost, 0)
def test_json_list_format(self):
config = {
'type': 'json',
'format': [
{'asctime': '@timestamp'},
{'levelname': 'level'},
'message'
],
'static_fields': {
'app': 'patroni'
}
}
test_message = 'test json logging in case of list format'
with patch('sys.stderr', StringIO()) as stderr_output:
logger = PatroniLogger()
logger.reload_config(config)
_LOG.info(test_message)
target_log = json.loads(stderr_output.getvalue())
self.assertIn('@timestamp', target_log)
self.assertEqual(target_log['message'], test_message)
self.assertEqual(target_log['level'], 'INFO')
self.assertEqual(target_log['app'], 'patroni')
self.assertEqual(len(target_log), len(config['format']) + len(config['static_fields']))
def test_json_str_format(self):
config = {
'type': 'json',
'format': '%(asctime)s %(levelname)s %(message)s',
'static_fields': {
'app': 'patroni'
}
}
test_message = 'test json logging in case of string format'
with patch('sys.stderr', StringIO()) as stderr_output:
logger = PatroniLogger()
logger.reload_config(config)
_LOG.info(test_message)
target_log = json.loads(stderr_output.getvalue())
self.assertIn('asctime', target_log)
self.assertEqual(target_log['message'], test_message)
self.assertEqual(target_log['levelname'], 'INFO')
self.assertEqual(target_log['app'], 'patroni')
def test_plain_format(self):
config = {
'type': 'plain',
'format': '[%(asctime)s] %(levelname)s %(message)s',
}
test_message = 'test plain logging'
with patch('sys.stderr', StringIO()) as stderr_output:
logger = PatroniLogger()
logger.reload_config(config)
_LOG.info(test_message)
target_log = stderr_output.getvalue()
self.assertRegex(target_log, fr'^\[.*\] INFO {test_message}$')
def test_dateformat(self):
config = {
'format': '[%(asctime)s] %(message)s',
'dateformat': '%Y-%m-%dT%H:%M:%S'
}
test_message = 'test date format'
with patch('sys.stderr', StringIO()) as stderr_output:
logger = PatroniLogger()
logger.reload_config(config)
_LOG.info(test_message)
target_log = stderr_output.getvalue()
self.assertRegex(target_log, r'\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\]')
def test_invalid_dateformat(self):
config = {
'format': '[%(asctime)s] %(message)s',
'dateformat': 5
}
with self.assertLogs() as captured_log:
logger = PatroniLogger()
logger.reload_config(config)
captured_log_level = captured_log.records[0].levelname
captured_log_message = captured_log.records[0].message
self.assertEqual(captured_log_level, 'WARNING')
self.assertRegex(
captured_log_message,
fr'Expected log dateformat to be a string, but got "{type(config["dateformat"])}"'
)
def test_invalid_plain_format(self):
config = {
'type': 'plain',
'format': ['message']
}
with self.assertLogs() as captured_log:
logger = PatroniLogger()
logger.reload_config(config)
captured_log_level = captured_log.records[0].levelname
captured_log_message = captured_log.records[0].message
self.assertEqual(captured_log_level, 'WARNING')
self.assertRegex(
captured_log_message,
r'Expected log format to be a string when log type is plain, but got ".*"'
)
def test_invalid_json_format(self):
config = {
'type': 'json',
'format': {
'asctime': 'timestamp',
'message': 'message'
}
}
with self.assertLogs() as captured_log:
logger = PatroniLogger()
logger.reload_config(config)
captured_log_level = captured_log.records[0].levelname
captured_log_message = captured_log.records[0].message
self.assertEqual(captured_log_level, 'WARNING')
self.assertRegex(
captured_log_message,
r'Expected log format to be a string or a list, but got ".*"'
)
with self.assertLogs() as captured_log:
config['format'] = ['message', ['levelname']]
logger.reload_config(config)
captured_log_level = captured_log.records[0].levelname
captured_log_message = captured_log.records[0].message
self.assertEqual(captured_log_level, 'WARNING')
self.assertRegex(
captured_log_message,
r'Expected each item of log format to be a string or dictionary, but got ".*"'
)
with self.assertLogs() as captured_log:
config['format'] = [
'message',
{'asctime': ['timestamp']}
]
logger.reload_config(config)
captured_log_level = captured_log.records[0].levelname
captured_log_message = captured_log.records[0].message
self.assertEqual(captured_log_level, 'WARNING')
self.assertRegex(
captured_log_message,
r'Expected renamed log field to be a string, but got ".*"'
)
@patch('pythonjsonlogger.jsonlogger.JsonFormatter', side_effect=ImportError)
def test_fail_to_import_python_json_logger(self, _):
config = {
'type': 'json'
}
with self.assertLogs() as captured_log:
logger = PatroniLogger()
logger.reload_config(config)
captured_log_level = captured_log.records[0].levelname
captured_log_message = captured_log.records[0].message
self.assertEqual(captured_log_level, 'ERROR')
self.assertRegex(
captured_log_message,
r'Failed to import "python-json-logger" library. Falling back to the plain logger'
)