Skip to content

Commit f099c63

Browse files
basic logging
1 parent 601f66a commit f099c63

File tree

2 files changed

+175
-7
lines changed

2 files changed

+175
-7
lines changed
+42-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,47 @@
11
"""
22
Use the influxdb_client together with python native logging
33
"""
4+
import logging
45

6+
from influxdb_client import InfluxDBClient
57

6-
class InfluxLoggingHandler:
7-
pass
8+
9+
class InfluxLoggingHandler(logging.Handler):
10+
DEFAULT_LOG_RECORD_KEYS = logging.makeLogRecord({}).__dict__.keys()
11+
12+
def __init__(self, *, url, token, org, bucket, client_args=None, write_api_args=None):
13+
super().__init__()
14+
15+
self.bucket = bucket
16+
17+
client_args = {} if client_args is None else client_args
18+
self.client = InfluxDBClient(url=url, token=token, org=org, **client_args)
19+
20+
write_api_args = {} if write_api_args is None else write_api_args
21+
self.write_api = self.client.write_api(**write_api_args)
22+
23+
def __del__(self):
24+
self.close()
25+
26+
def close(self) -> None:
27+
self.write_api.close()
28+
self.client.close()
29+
super().close()
30+
31+
def emit(self, record: logging.LogRecord) -> None:
32+
""" Emit a record via the influxDB WriteApi """
33+
try:
34+
extra = self._get_extra_values(record)
35+
return self.write_api.write(record=record.msg, **extra)
36+
except (KeyboardInterrupt, SystemExit):
37+
raise
38+
except (Exception,):
39+
self.handleError(record)
40+
41+
def _get_extra_values(self, record: logging.LogRecord) -> dict:
42+
"""extracts all items from the record that were injected by logging.debug(msg, extra={key: value, ...})"""
43+
extra = {key: value
44+
for key, value in record.__dict__.items() if key not in self.DEFAULT_LOG_RECORD_KEYS}
45+
if 'bucket' not in extra.keys():
46+
extra['bucket'] = self.bucket
47+
return extra

tests/test_loggingHandler.py

+133-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,137 @@
1+
import logging
12
import unittest
3+
import unittest.mock
24

3-
from influxdb_client import InfluxLoggingHandler
5+
import urllib3
46

7+
from influxdb_client import InfluxLoggingHandler, InfluxDBClient, WriteApi, WritePrecision, Point
58

6-
class LoggingHandlerTest(unittest.TestCase):
7-
def test_imports(self):
8-
handler = InfluxLoggingHandler()
9-
self.assertIsNotNone(handler)
9+
10+
class LoggingBaseTestCase(unittest.TestCase):
11+
fake_line_record = "tag,field=value 123456"
12+
URL_TOKEN_ORG = {
13+
'url': 'http://example.com',
14+
'token': 'my-token',
15+
'org': 'my-org',
16+
}
17+
BUCKET = 'my-bucket'
18+
19+
def setUp(self) -> None:
20+
self.mock_InfluxDBClient = unittest.mock.patch("influxdb_client.client.loggingHandler.InfluxDBClient").start()
21+
self.mock_client = unittest.mock.MagicMock(spec=InfluxDBClient)
22+
self.mock_write_api = unittest.mock.MagicMock(spec=WriteApi)
23+
self.mock_client.write_api.return_value = self.mock_write_api
24+
self.mock_InfluxDBClient.return_value = self.mock_client
25+
26+
def gen_handler_and_logger(self):
27+
self.handler = InfluxLoggingHandler(**self.URL_TOKEN_ORG, bucket=self.BUCKET)
28+
self.handler.setLevel(logging.DEBUG)
29+
30+
self.logger = logging.getLogger("test-logger")
31+
self.logger.setLevel(logging.DEBUG)
32+
33+
def tearDown(self) -> None:
34+
unittest.mock.patch.stopall()
35+
36+
37+
class TestHandlerCreation(LoggingBaseTestCase):
38+
def test_can_create_handler(self):
39+
self.handler = InfluxLoggingHandler(**self.URL_TOKEN_ORG, bucket=self.BUCKET)
40+
self.mock_InfluxDBClient.assert_called_once_with(**self.URL_TOKEN_ORG)
41+
self.assertEqual(self.BUCKET, self.handler.bucket)
42+
self.mock_client.write_api.assert_called_once_with()
43+
44+
def test_can_create_handler_with_optional_args_for_client(self):
45+
self.handler = InfluxLoggingHandler(**self.URL_TOKEN_ORG, bucket=self.BUCKET,
46+
client_args={'arg2': 2.90, 'optArg': 'whot'})
47+
self.mock_InfluxDBClient.assert_called_once_with(**self.URL_TOKEN_ORG, arg2=2.90, optArg="whot")
48+
self.mock_client.write_api.assert_called_once_with()
49+
50+
def test_can_create_handler_with_args_for_write_api(self):
51+
self.handler = InfluxLoggingHandler(**self.URL_TOKEN_ORG, bucket=self.BUCKET,
52+
client_args={'arg2': 2.90, 'optArg': 'whot'},
53+
write_api_args={'foo': 'bar'})
54+
self.mock_InfluxDBClient.assert_called_once_with(**self.URL_TOKEN_ORG, arg2=2.90, optArg="whot")
55+
self.mock_client.write_api.assert_called_once_with(foo='bar')
56+
57+
58+
class CreatedHandlerTestCase(LoggingBaseTestCase):
59+
def setUp(self) -> None:
60+
super().setUp()
61+
self.gen_handler_and_logger()
62+
63+
64+
class LoggingSetupAndTearDown(CreatedHandlerTestCase):
65+
def test_is_handler(self):
66+
self.assertIsInstance(self.handler, logging.Handler)
67+
68+
def test_set_up_client(self):
69+
self.mock_InfluxDBClient.assert_called_once()
70+
71+
def test_closes_connections_on_close(self):
72+
self.handler.close()
73+
self.mock_write_api.close.assert_called_once()
74+
self.mock_client.close.assert_called_once()
75+
76+
def test_handler_can_be_attached_to_logger(self):
77+
self.logger.addHandler(self.handler)
78+
self.assertTrue(self.logger.hasHandlers())
79+
self.assertTrue(self.handler in self.logger.handlers)
80+
81+
82+
class LoggingWithAttachedHandler(CreatedHandlerTestCase):
83+
84+
def setUp(self) -> None:
85+
super().setUp()
86+
self.logger.addHandler(self.handler)
87+
88+
89+
class LoggingHandlerTest(LoggingWithAttachedHandler):
90+
91+
def test_can_log_str(self):
92+
self.logger.debug(self.fake_line_record)
93+
self.mock_write_api.write.assert_called_once_with(bucket="my-bucket", record=self.fake_line_record)
94+
95+
def test_can_log_points(self):
96+
point = Point("measurement").field("field_name", "field_value").time(333, WritePrecision.NS)
97+
self.logger.debug(point)
98+
self.mock_write_api.write.assert_called_once_with(bucket="my-bucket", record=point)
99+
100+
def test_catches_urllib_exceptions(self):
101+
self.mock_write_api.write.side_effect = urllib3.exceptions.HTTPError()
102+
try:
103+
with unittest.mock.patch("logging.sys.stderr") as _:
104+
# Handler writes logging errors to stderr. Don't display it in the test output.
105+
self.logger.debug(self.fake_line_record)
106+
finally:
107+
self.mock_write_api.write.side_effect = None
108+
109+
def test_raises_on_exit(self):
110+
try:
111+
self.mock_write_api.write.side_effect = KeyboardInterrupt()
112+
self.assertRaises(KeyboardInterrupt, self.logger.debug, self.fake_line_record)
113+
self.mock_write_api.write.side_effect = SystemExit()
114+
self.assertRaises(SystemExit, self.logger.debug, self.fake_line_record)
115+
finally:
116+
self.mock_write_api.write.side_effect = None
117+
118+
def test_can_set_bucket(self):
119+
self.handler.bucket = "new-bucket"
120+
self.logger.debug(self.fake_line_record)
121+
self.mock_write_api.write.assert_called_once_with(bucket="new-bucket", record=self.fake_line_record)
122+
123+
def test_can_pass_bucket_on_log(self):
124+
self.logger.debug(self.fake_line_record, extra={'bucket': "other-bucket"})
125+
self.mock_write_api.write.assert_called_once_with(bucket="other-bucket", record=self.fake_line_record)
126+
127+
def test_can_pass_optional_params_on_log(self):
128+
self.logger.debug(self.fake_line_record, extra={'org': "other-org", 'write_precision': WritePrecision.S,
129+
"arg3": 3, "arg2": "two"})
130+
self.mock_write_api.write.assert_called_once_with(bucket="my-bucket", org='other-org',
131+
record=self.fake_line_record,
132+
write_precision=WritePrecision.S,
133+
arg3=3, arg2="two")
134+
135+
136+
if __name__ == "__main__":
137+
unittest.main()

0 commit comments

Comments
 (0)