Skip to content

Commit 54d8110

Browse files
authored
[4.4] Add documentation and tests for logging (#738)
* Add documentation for logging * Add unit tests for logging helper
1 parent e10bb44 commit 54d8110

File tree

3 files changed

+239
-12
lines changed

3 files changed

+239
-12
lines changed

docs/source/api.rst

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1413,6 +1413,34 @@ following code:
14131413
...
14141414
14151415
1416+
*******
1417+
Logging
1418+
*******
1419+
1420+
The driver offers logging for debugging purposes. It is not recommended to
1421+
enable logging for anything other than debugging. For instance, if the driver is
1422+
not able to connect to the database server or if undesired behavior is observed.
1423+
1424+
Logging can be enable like so:
1425+
1426+
.. code-block:: python
1427+
1428+
import logging
1429+
import sys
1430+
1431+
# create a handler, e.g. to log to stdout
1432+
handler = logging.StreamHandler(sys.stdout)
1433+
# configure the handler to your liking
1434+
handler.setFormatter(logging.Formatter(
1435+
"%(threadName)s(%(thread)d) %(asctime)s %(message)s"
1436+
))
1437+
# add the handler to the driver's logger
1438+
logging.getLogger("neo4j").addHandler(handler)
1439+
# make sure the logger logs on the desired log level
1440+
logging.getLogger("neo4j").setLevel(logging.DEBUG)
1441+
# from now on, DEBUG logging to stderr is enabled in the driver
1442+
1443+
14161444
********
14171445
Bookmark
14181446
********

neo4j/debug.py

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,30 @@ def format(self, record):
4444

4545

4646
class Watcher:
47-
""" Log watcher for monitoring driver and protocol activity.
48-
"""
47+
"""Log watcher for easier logging setup.
48+
49+
Example::
50+
51+
from neo4j.debug import Watcher
4952
50-
handlers = {}
53+
with Watcher("neo4j"):
54+
# DEBUG logging to stderr enabled within this context
55+
... # do something
56+
57+
.. note:: The Watcher class is not thread-safe. Having Watchers in multiple
58+
threads can lead to duplicate log messages as the context manager will
59+
enable logging for all threads.
60+
61+
:param logger_names: Names of loggers to watch.
62+
:type logger_names: str
63+
"""
5164

5265
def __init__(self, *logger_names):
5366
super(Watcher, self).__init__()
5467
self.logger_names = logger_names
5568
self.loggers = [getLogger(name) for name in self.logger_names]
5669
self.formatter = ColourFormatter("%(asctime)s %(message)s")
70+
self.handlers = {}
5771

5872
def __enter__(self):
5973
self.watch()
@@ -63,6 +77,13 @@ def __exit__(self, exc_type, exc_val, exc_tb):
6377
self.stop()
6478

6579
def watch(self, level=DEBUG, out=stderr):
80+
"""Enable logging for all loggers.
81+
82+
:param level: Minimum log level to show.
83+
:type level: int
84+
:param out: Output stream for all loggers.
85+
:type out: stream or file-like object
86+
"""
6687
self.stop()
6788
handler = StreamHandler(out)
6889
handler.setFormatter(self.formatter)
@@ -72,20 +93,36 @@ def watch(self, level=DEBUG, out=stderr):
7293
logger.setLevel(level)
7394

7495
def stop(self):
75-
try:
76-
for logger in self.loggers:
77-
logger.removeHandler(self.handlers[logger.name])
78-
except KeyError:
79-
pass
96+
"""Disable logging for all loggers."""
97+
for logger in self.loggers:
98+
try:
99+
logger.removeHandler(self.handlers.pop(logger.name))
100+
except KeyError:
101+
pass
80102

81103

82104
def watch(*logger_names, level=DEBUG, out=stderr):
83-
""" Quick wrapper for using the Watcher.
105+
"""Quick wrapper for using :class:`.Watcher`.
106+
107+
Create a Wathcer with the given configuration, enable watching and return
108+
it.
109+
110+
Example::
111+
112+
from neo4j.debug import watch
113+
114+
watch("neo4j")
115+
# from now on, DEBUG logging to stderr is enabled in the driver
116+
117+
:param logger_names: name of logger to watch
118+
:type logger_names: str
119+
:param level: minimum log level to show (default ``logging.DEBUG``)
120+
:type level: int
121+
:param out: where to send output (default ``sys.stderr``)
122+
:type out: stream or file-like object
84123
85-
:param logger_name: name of logger to watch
86-
:param level: minimum log level to show (default DEBUG)
87-
:param out: where to send output (default stderr)
88124
:return: Watcher instance
125+
:rtype: :class:`.Watcher`
89126
"""
90127
watcher = Watcher(*logger_names)
91128
watcher.watch(level, out)

tests/unit/test_debug.py

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# Copyright (c) "Neo4j"
2+
# Neo4j Sweden AB [https://neo4j.com]
3+
#
4+
# This file is part of Neo4j.
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# https://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
18+
19+
import io
20+
import logging
21+
import sys
22+
23+
import pytest
24+
25+
from neo4j import debug as neo4j_debug
26+
27+
28+
@pytest.fixture
29+
def add_handler_mocker(mocker):
30+
def setup_mock(*logger_names):
31+
loggers = [logging.getLogger(name) for name in logger_names]
32+
for logger in loggers:
33+
logger.addHandler = mocker.Mock()
34+
logger.removeHandler = mocker.Mock()
35+
logger.setLevel = mocker.Mock()
36+
return loggers
37+
38+
return setup_mock
39+
40+
41+
def test_watch_returns_watcher(add_handler_mocker):
42+
logger_name = "neo4j"
43+
add_handler_mocker(logger_name)
44+
watcher = neo4j_debug.watch(logger_name)
45+
assert isinstance(watcher, neo4j_debug.Watcher)
46+
47+
48+
@pytest.mark.parametrize("logger_names",
49+
(("neo4j",), ("foobar",), ("neo4j", "foobar")))
50+
def test_watch_enables_logging(logger_names, add_handler_mocker):
51+
loggers = add_handler_mocker(*logger_names)
52+
neo4j_debug.watch(*logger_names)
53+
for logger in loggers:
54+
logger.addHandler.assert_called_once()
55+
56+
57+
def test_watcher_watch_adds_logger(add_handler_mocker):
58+
logger_name = "neo4j"
59+
logger = add_handler_mocker(logger_name)[0]
60+
watcher = neo4j_debug.Watcher(logger_name)
61+
62+
logger.addHandler.assert_not_called()
63+
watcher.watch()
64+
logger.addHandler.assert_called_once()
65+
66+
67+
def test_watcher_stop_removes_logger(add_handler_mocker):
68+
logger_name = "neo4j"
69+
logger = add_handler_mocker(logger_name)[0]
70+
watcher = neo4j_debug.Watcher(logger_name)
71+
72+
watcher.watch()
73+
(handler,), _ = logger.addHandler.call_args
74+
75+
logger.removeHandler.assert_not_called()
76+
watcher.stop()
77+
logger.removeHandler.assert_called_once_with(handler)
78+
79+
80+
def test_watcher_context_manager(mocker):
81+
logger_name = "neo4j"
82+
watcher = neo4j_debug.Watcher(logger_name)
83+
watcher.watch = mocker.Mock()
84+
watcher.stop = mocker.Mock()
85+
86+
with watcher:
87+
watcher.watch.assert_called_once()
88+
watcher.stop.assert_not_called()
89+
watcher.stop.assert_called_once()
90+
91+
92+
@pytest.mark.parametrize(
93+
("level", "expected_level"),
94+
(
95+
(None, logging.DEBUG),
96+
(logging.DEBUG, logging.DEBUG),
97+
(logging.WARNING, logging.WARNING),
98+
(1, 1),
99+
)
100+
)
101+
def test_watcher_level(add_handler_mocker, level, expected_level):
102+
logger_name = "neo4j"
103+
logger = add_handler_mocker(logger_name)[0]
104+
watcher = neo4j_debug.Watcher(logger_name)
105+
kwargs = {}
106+
if level is not None:
107+
kwargs["level"] = level
108+
watcher.watch(**kwargs)
109+
110+
(handler,), _ = logger.addHandler.call_args
111+
assert handler.level == logging.NOTSET
112+
logger.setLevel.assert_called_once_with(expected_level)
113+
114+
115+
custom_log_out = io.StringIO()
116+
117+
@pytest.mark.parametrize(
118+
("out", "expected_out"),
119+
(
120+
(None, sys.stderr),
121+
(sys.stderr, sys.stderr),
122+
(sys.stdout, sys.stdout),
123+
(custom_log_out, custom_log_out),
124+
)
125+
)
126+
def test_watcher_out(add_handler_mocker, out, expected_out):
127+
logger_name = "neo4j"
128+
logger = add_handler_mocker(logger_name)[0]
129+
watcher = neo4j_debug.Watcher(logger_name)
130+
kwargs = {}
131+
if out is not None:
132+
kwargs["out"] = out
133+
watcher.watch(**kwargs)
134+
135+
(handler,), _ = logger.addHandler.call_args
136+
assert isinstance(handler, logging.StreamHandler)
137+
assert handler.stream == expected_out
138+
139+
140+
def test_watcher_colour(add_handler_mocker):
141+
logger_name = "neo4j"
142+
logger = add_handler_mocker(logger_name)[0]
143+
watcher = neo4j_debug.Watcher(logger_name)
144+
watcher.watch()
145+
146+
(handler,), _ = logger.addHandler.call_args
147+
assert isinstance(handler, logging.Handler)
148+
assert isinstance(handler.formatter, neo4j_debug.ColourFormatter)
149+
150+
151+
def test_watcher_format(add_handler_mocker):
152+
logger_name = "neo4j"
153+
logger = add_handler_mocker(logger_name)[0]
154+
watcher = neo4j_debug.Watcher(logger_name)
155+
watcher.watch()
156+
157+
(handler,), _ = logger.addHandler.call_args
158+
assert isinstance(handler, logging.Handler)
159+
assert isinstance(handler.formatter, logging.Formatter)
160+
# Don't look at me like that. It's just for testing.
161+
format = handler.formatter._fmt
162+
assert format == "%(asctime)s %(message)s"

0 commit comments

Comments
 (0)