Skip to content

Commit fe37b7d

Browse files
authored
Add support for Bolt 5.4 an API telemetry (#965)
Starting with Bolt 5.4, the driver will, by default, send anonymous API usage statistics to the server if requested. The driver configuration `telemetry_disabled=True` can be used to disable this. ```python import neo4j with neo4j.GraphDatabase.driver(..., telemetry_disabled=True) as driver: ... ``` The driver transmits the following information: * Every time one of the following APIs is used to execute a query (for the first time), the server is informed of this (without any further information like arguments, client identifiers, etc.): * `driver.execute_query` * `session.begin_transaction` * `session.execute_read`, `session.execute_write` * `session.run` * the async counterparts of the above methods
1 parent 93805db commit fe37b7d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+2108
-96
lines changed

docs/source/api.rst

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,7 @@ Additional configuration can be provided via the :class:`neo4j.Driver` construct
399399
+ :ref:`user-agent-ref`
400400
+ :ref:`driver-notifications-min-severity-ref`
401401
+ :ref:`driver-notifications-disabled-categories-ref`
402+
+ :ref:`telemetry-disabled-ref`
402403

403404

404405
.. _connection-acquisition-timeout-ref:
@@ -664,6 +665,30 @@ Notifications are available via :attr:`.ResultSummary.notifications` and :attr:`
664665
.. seealso:: :class:`.NotificationDisabledCategory`, session config :ref:`session-notifications-disabled-categories-ref`
665666

666667

668+
.. _telemetry-disabled-ref:
669+
670+
``telemetry_disabled``
671+
----------------------
672+
By default, the driver will send anonymous usage statistics to the server it connects to if the server requests those.
673+
By setting ``telemetry_disabled=True``, the driver will not send any telemetry data.
674+
675+
The driver transmits the following information:
676+
677+
* Every time one of the following APIs is used to execute a query (for the first time), the server is informed of this
678+
(without any further information like arguments, client identifiers, etc.):
679+
680+
* :meth:`.Driver.execute_query`
681+
* :meth:`.Session.begin_transaction`
682+
* :meth:`.Session.execute_read`, :meth:`.Session.execute_write`
683+
* :meth:`.Session.run`
684+
* the async counterparts of the above methods
685+
686+
:Type: :class:`bool`
687+
:Default: :data:`False`
688+
689+
.. versionadded:: 5.13
690+
691+
667692
Driver Object Lifetime
668693
======================
669694

src/neo4j/_api.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"NotificationCategory",
3333
"NotificationSeverity",
3434
"RoutingControl",
35+
"TelemetryAPI"
3536
]
3637

3738

@@ -227,6 +228,13 @@ class RoutingControl(str, Enum):
227228
WRITE = "w"
228229

229230

231+
class TelemetryAPI(int, Enum):
232+
TX_FUNC = 0
233+
TX = 1
234+
AUTO_COMMIT = 2
235+
DRIVER = 3
236+
237+
230238
if t.TYPE_CHECKING:
231239
T_RoutingControl = t.Union[
232240
RoutingControl,

src/neo4j/_async/driver.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@
3131
T_NotificationMinimumSeverity,
3232
)
3333

34-
from .._api import RoutingControl
34+
from .._api import (
35+
RoutingControl,
36+
TelemetryAPI,
37+
)
3538
from .._async_compat.util import AsyncUtil
3639
from .._conf import (
3740
Config,
@@ -71,6 +74,7 @@
7174
URI_SCHEME_NEO4J,
7275
URI_SCHEME_NEO4J_SECURE,
7376
URI_SCHEME_NEO4J_SELF_SIGNED_CERTIFICATE,
77+
WRITE_ACCESS,
7478
)
7579
from ..auth_management import (
7680
AsyncAuthManager,
@@ -159,7 +163,8 @@ def driver(
159163
fetch_size: int = ...,
160164
impersonated_user: t.Optional[str] = ...,
161165
bookmark_manager: t.Union[AsyncBookmarkManager,
162-
BookmarkManager, None] = ...
166+
BookmarkManager, None] = ...,
167+
telemetry_disabled: bool = ...,
163168
) -> AsyncDriver:
164169
...
165170

@@ -866,15 +871,16 @@ async def example(driver: neo4j.AsyncDriver) -> neo4j.Record::
866871
session = self._session(session_config)
867872
async with session:
868873
if routing_ == RoutingControl.WRITE:
869-
executor = session.execute_write
874+
access_mode = WRITE_ACCESS
870875
elif routing_ == RoutingControl.READ:
871-
executor = session.execute_read
876+
access_mode = READ_ACCESS
872877
else:
873878
raise ValueError("Invalid routing control value: %r"
874879
% routing_)
875880
with session._pipelined_begin:
876-
return await executor(
877-
_work, query_, parameters, result_transformer_
881+
return await session._run_transaction(
882+
access_mode, TelemetryAPI.DRIVER,
883+
_work, (query_, parameters, result_transformer_), {}
878884
)
879885

880886
@property

src/neo4j/_async/io/_bolt.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from logging import getLogger
2626
from time import perf_counter
2727

28+
from ..._api import TelemetryAPI
2829
from ..._async_compat.network import AsyncBoltSocket
2930
from ..._async_compat.util import AsyncUtil
3031
from ..._codec.hydration import v1 as hydration_v1
@@ -134,7 +135,8 @@ class AsyncBolt:
134135
def __init__(self, unresolved_address, sock, max_connection_lifetime, *,
135136
auth=None, auth_manager=None, user_agent=None,
136137
routing_context=None, notifications_min_severity=None,
137-
notifications_disabled_categories=None):
138+
notifications_disabled_categories=None,
139+
telemetry_disabled=False):
138140
self.unresolved_address = unresolved_address
139141
self.socket = sock
140142
self.local_port = self.socket.getsockname()[1]
@@ -172,6 +174,7 @@ def __init__(self, unresolved_address, sock, max_connection_lifetime, *,
172174
self.auth = auth
173175
self.auth_dict = self._to_auth_dict(auth)
174176
self.auth_manager = auth_manager
177+
self.telemetry_disabled = telemetry_disabled
175178

176179
self.notifications_min_severity = notifications_min_severity
177180
self.notifications_disabled_categories = \
@@ -280,6 +283,7 @@ def protocol_handlers(cls, protocol_version=None):
280283
AsyncBolt5x1,
281284
AsyncBolt5x2,
282285
AsyncBolt5x3,
286+
AsyncBolt5x4,
283287
)
284288

285289
handlers = {
@@ -293,6 +297,7 @@ def protocol_handlers(cls, protocol_version=None):
293297
AsyncBolt5x1.PROTOCOL_VERSION: AsyncBolt5x1,
294298
AsyncBolt5x2.PROTOCOL_VERSION: AsyncBolt5x2,
295299
AsyncBolt5x3.PROTOCOL_VERSION: AsyncBolt5x3,
300+
AsyncBolt5x4.PROTOCOL_VERSION: AsyncBolt5x4,
296301
}
297302

298303
if protocol_version is None:
@@ -407,7 +412,10 @@ async def open(
407412

408413
# Carry out Bolt subclass imports locally to avoid circular dependency
409414
# issues.
410-
if protocol_version == (5, 3):
415+
if protocol_version == (5, 4):
416+
from ._bolt5 import AsyncBolt5x4
417+
bolt_cls = AsyncBolt5x4
418+
elif protocol_version == (5, 3):
411419
from ._bolt5 import AsyncBolt5x3
412420
bolt_cls = AsyncBolt5x3
413421
elif protocol_version == (5, 2):
@@ -471,7 +479,8 @@ async def open(
471479
routing_context=routing_context,
472480
notifications_min_severity=pool_config.notifications_min_severity,
473481
notifications_disabled_categories=
474-
pool_config.notifications_disabled_categories
482+
pool_config.notifications_disabled_categories,
483+
telemetry_disabled=pool_config.telemetry_disabled,
475484
)
476485

477486
try:
@@ -555,7 +564,6 @@ def re_auth(
555564
hydration_hooks=hydration_hooks)
556565
return True
557566

558-
559567
@abc.abstractmethod
560568
async def route(
561569
self, database=None, imp_user=None, bookmarks=None,
@@ -584,6 +592,23 @@ async def route(
584592
"""
585593
pass
586594

595+
@abc.abstractmethod
596+
def telemetry(self, api: TelemetryAPI, dehydration_hooks=None,
597+
hydration_hooks=None, **handlers) -> None:
598+
"""Send telemetry information about the API usage to the server.
599+
600+
:param api: the API used.
601+
:param dehydration_hooks:
602+
Hooks to dehydrate types (dict from type (class) to dehydration
603+
function). Dehydration functions receive the value and returns an
604+
object of type understood by packstream.
605+
:param hydration_hooks:
606+
Hooks to hydrate types (mapping from type (class) to
607+
dehydration function). Dehydration functions receive the value of
608+
type understood by packstream and are free to return anything.
609+
"""
610+
pass
611+
587612
@abc.abstractmethod
588613
def run(self, query, parameters=None, mode=None, bookmarks=None,
589614
metadata=None, timeout=None, db=None, imp_user=None,

src/neo4j/_async/io/_bolt3.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from logging import getLogger
2424
from ssl import SSLSocket
2525

26+
from ..._api import TelemetryAPI
2627
from ..._exceptions import BoltProtocolError
2728
from ...api import (
2829
READ_ACCESS,
@@ -225,6 +226,11 @@ def logoff(self, dehydration_hooks=None, hydration_hooks=None):
225226
"""Append a LOGOFF message to the outgoing queue."""
226227
self.assert_re_auth_support()
227228

229+
def telemetry(self, api: TelemetryAPI, dehydration_hooks=None,
230+
hydration_hooks=None, **handlers) -> None:
231+
# TELEMETRY not support by this protocol version, so we ignore it.
232+
pass
233+
228234
async def route(
229235
self, database=None, imp_user=None, bookmarks=None,
230236
dehydration_hooks=None, hydration_hooks=None

src/neo4j/_async/io/_bolt4.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from logging import getLogger
2020
from ssl import SSLSocket
2121

22+
from ..._api import TelemetryAPI
2223
from ..._exceptions import BoltProtocolError
2324
from ...api import (
2425
READ_ACCESS,
@@ -146,6 +147,11 @@ def logoff(self, dehydration_hooks=None, hydration_hooks=None):
146147
"""Append a LOGOFF message to the outgoing queue."""
147148
self.assert_re_auth_support()
148149

150+
def telemetry(self, api: TelemetryAPI, dehydration_hooks=None,
151+
hydration_hooks=None, **handlers) -> None:
152+
# TELEMETRY not support by this protocol version, so we ignore it.
153+
pass
154+
149155
async def route(
150156
self, database=None, imp_user=None, bookmarks=None,
151157
dehydration_hooks=None, hydration_hooks=None

src/neo4j/_async/io/_bolt5.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from logging import getLogger
2222
from ssl import SSLSocket
2323

24+
from ..._api import TelemetryAPI
2425
from ..._codec.hydration import v2 as hydration_v2
2526
from ..._exceptions import BoltProtocolError
2627
from ..._meta import BOLT_AGENT_DICT
@@ -168,6 +169,11 @@ def logoff(self, dehydration_hooks=None, hydration_hooks=None):
168169
"""Append a LOGOFF message to the outgoing queue."""
169170
self.assert_re_auth_support()
170171

172+
def telemetry(self, api: TelemetryAPI, dehydration_hooks=None,
173+
hydration_hooks=None, **handlers) -> None:
174+
# TELEMETRY not support by this protocol version, so we ignore it.
175+
pass
176+
171177
async def route(self, database=None, imp_user=None, bookmarks=None,
172178
dehydration_hooks=None, hydration_hooks=None):
173179
routing_context = self.routing_context or {}
@@ -665,3 +671,22 @@ def get_base_headers(self):
665671
headers = super().get_base_headers()
666672
headers["bolt_agent"] = BOLT_AGENT_DICT
667673
return headers
674+
675+
676+
class AsyncBolt5x4(AsyncBolt5x3):
677+
678+
PROTOCOL_VERSION = Version(5, 4)
679+
680+
def telemetry(self, api: TelemetryAPI, dehydration_hooks=None,
681+
hydration_hooks=None, **handlers) -> None:
682+
if (
683+
self.telemetry_disabled
684+
or not self.configuration_hints.get("telemetry.enabled", False)
685+
):
686+
return
687+
api_raw = int(api)
688+
log.debug("[#%04X] C: TELEMETRY %i # (%r)",
689+
self.local_port, api_raw, api)
690+
self._append(b"\x54", (api_raw,),
691+
Response(self, "telemetry", hydration_hooks, **handlers),
692+
dehydration_hooks=dehydration_hooks)

0 commit comments

Comments
 (0)