From b837c9f6278fbace77b4d60acb540209f50aac37 Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Thu, 26 Sep 2019 13:40:42 -0700 Subject: [PATCH 01/20] Span add override parameters for start_time and end_time --- opentelemetry-api/src/opentelemetry/trace/__init__.py | 4 ++-- .../src/opentelemetry/sdk/trace/__init__.py | 8 ++++++-- opentelemetry-sdk/tests/trace/test_trace.py | 10 ++++++++++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index b79cdeb4df5..55cdc567d5d 100644 --- a/opentelemetry-api/src/opentelemetry/trace/__init__.py +++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py @@ -142,7 +142,7 @@ class SpanKind(enum.Enum): class Span: """A span represents a single operation within a trace.""" - def start(self) -> None: + def start(self, start_time: int = None) -> None: """Sets the current time as the span's start time. Each span represents a single operation. The span's start time is the @@ -152,7 +152,7 @@ def start(self) -> None: implementations are free to ignore or raise on further calls. """ - def end(self) -> None: + def end(self, end_time: int = None) -> None: """Sets the current time as the span's end time. The span's end time is the wall time at which the operation finished. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index a694476e1fe..37f9c35e07a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -340,19 +340,21 @@ def add_lazy_link(self, link: "trace_api.Link") -> None: return self.links.append(link) - def start(self): + def start(self, start_time: int = None): with self._lock: if not self.is_recording_events(): return has_started = self.start_time is not None if not has_started: self.start_time = util.time_ns() + if start_time is not None : + self.start_time = start_time if has_started: logger.warning("Calling start() on a started span.") return self.span_processor.on_start(self) - def end(self): + def end(self, end_time: int = None): with self._lock: if not self.is_recording_events(): return @@ -361,6 +363,8 @@ def end(self): has_ended = self.end_time is not None if not has_ended: self.end_time = util.time_ns() + if end_time is not None: + self.end_time = end_time if has_ended: logger.warning("Calling end() on an ended span.") return diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 0570affc411..378534453c2 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -234,6 +234,16 @@ def test_start_span(self): span.start() self.assertEqual(start_time, span.start_time) + def test_span_override_start_and_end_time(self): + """Span sending custom start_time and end_time values""" + span = trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) + start_time = 123 + span.start(start_time) + self.assertEqual(start_time, span.start_time) + end_time = 456 + span.end(end_time) + self.assertEqual(end_time, span.end_time) + def test_ended_span(self): """"Events, attributes are not allowed after span is ended""" tracer = trace.Tracer("test_ended_span") From a12fe5dab59cc38414e8e6795f4de9ffd0c694ee Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Thu, 26 Sep 2019 16:26:00 -0700 Subject: [PATCH 02/20] Make lint happy --- opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 37f9c35e07a..e844bfcf94f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -347,7 +347,7 @@ def start(self, start_time: int = None): has_started = self.start_time is not None if not has_started: self.start_time = util.time_ns() - if start_time is not None : + if start_time is not None: self.start_time = start_time if has_started: logger.warning("Calling start() on a started span.") From 276ecb4138ac1a772be41c5fd35b8d3ddec9a1ad Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Fri, 27 Sep 2019 11:27:19 -0700 Subject: [PATCH 03/20] Addressing comments --- .../src/opentelemetry/sdk/trace/__init__.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index e844bfcf94f..1e1594e348d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -346,9 +346,10 @@ def start(self, start_time: int = None): return has_started = self.start_time is not None if not has_started: - self.start_time = util.time_ns() - if start_time is not None: - self.start_time = start_time + if start_time is not None: + self.start_time = start_time + else: + self.start_time = util.time_ns() if has_started: logger.warning("Calling start() on a started span.") return @@ -362,9 +363,10 @@ def end(self, end_time: int = None): raise RuntimeError("Calling end() on a not started span.") has_ended = self.end_time is not None if not has_ended: - self.end_time = util.time_ns() - if end_time is not None: - self.end_time = end_time + if end_time is not None: + self.end_time = end_time + else: + self.end_time = util.time_ns() if has_ended: logger.warning("Calling end() on an ended span.") return From 2b90351052f8edb389be395a88e0fa38b606079d Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Fri, 27 Sep 2019 14:10:59 -0700 Subject: [PATCH 04/20] Addressing comments --- .../src/opentelemetry/sdk/trace/__init__.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 1e1594e348d..23620146816 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -346,10 +346,7 @@ def start(self, start_time: int = None): return has_started = self.start_time is not None if not has_started: - if start_time is not None: - self.start_time = start_time - else: - self.start_time = util.time_ns() + self.start_time = start_time or util.time_ns() if has_started: logger.warning("Calling start() on a started span.") return @@ -363,10 +360,7 @@ def end(self, end_time: int = None): raise RuntimeError("Calling end() on a not started span.") has_ended = self.end_time is not None if not has_ended: - if end_time is not None: - self.end_time = end_time - else: - self.end_time = util.time_ns() + self.end_time = end_time or util.time_ns() if has_ended: logger.warning("Calling end() on an ended span.") return From eccef1a9686f2527335cfe5b18e22a1e79e84b90 Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Fri, 27 Sep 2019 17:06:21 -0700 Subject: [PATCH 05/20] Allowing 0 as start and end time --- opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 23620146816..d24955d1639 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -346,7 +346,7 @@ def start(self, start_time: int = None): return has_started = self.start_time is not None if not has_started: - self.start_time = start_time or util.time_ns() + self.start_time = start_time if start_time is not None else util.time_ns() if has_started: logger.warning("Calling start() on a started span.") return @@ -360,7 +360,7 @@ def end(self, end_time: int = None): raise RuntimeError("Calling end() on a not started span.") has_ended = self.end_time is not None if not has_ended: - self.end_time = end_time or util.time_ns() + self.end_time = end_time if end_time is not None else util.time_ns() if has_ended: logger.warning("Calling end() on an ended span.") return From a187fec187ce06bcf78bfbd64195cfba783a31cd Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Fri, 27 Sep 2019 17:16:43 -0700 Subject: [PATCH 06/20] Fix lint issues --- opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index d24955d1639..37294ee05f7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -346,7 +346,9 @@ def start(self, start_time: int = None): return has_started = self.start_time is not None if not has_started: - self.start_time = start_time if start_time is not None else util.time_ns() + self.start_time = ( + start_time if start_time is not None else util.time_ns() + ) if has_started: logger.warning("Calling start() on a started span.") return @@ -360,7 +362,9 @@ def end(self, end_time: int = None): raise RuntimeError("Calling end() on a not started span.") has_ended = self.end_time is not None if not has_ended: - self.end_time = end_time if end_time is not None else util.time_ns() + self.end_time = ( + end_time if end_time is not None else util.time_ns() + ) if has_ended: logger.warning("Calling end() on an ended span.") return From 77d364946040ea6125c8028933edf1702fc49960 Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Tue, 8 Oct 2019 11:59:29 -0700 Subject: [PATCH 07/20] Add code coverage --- .codecov.yml | 5 +++++ .gitignore | 1 + .travis.yml | 8 ++++++++ tox.ini | 5 ++++- 4 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 .codecov.yml diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 00000000000..77194778197 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,5 @@ +fixes: + - "^.*/site-packages/opentelemetry/sdk/::opentelemetry-sdk/src/opentelemetry/sdk/" + - "^.*/site-packages/opentelemetry/ext/wsgi/::ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/" + - "^.*/site-packages/opentelemetry/ext/http_requests/::ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/" + - "^.*/site-packages/opentelemetry/::opentelemetry-api/src/opentelemetry/" diff --git a/.gitignore b/.gitignore index 679b6fd0ccf..9a72cbf8cef 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ pip-log.txt .tox .cache htmlcov +coverage.xml # Translations *.mo diff --git a/.travis.yml b/.travis.yml index 64eebc36213..6e5c9e2039c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,3 +19,11 @@ install: script: - tox + +after_success: + - pip install codecov + - codecov -v --file + opentelemetry-api/tests/coverage.xml + opentelemetry-sdk/tests/coverage.xml + ext/opentelemetry-ext-wsgi/tests/coverage.xml + ext/opentelemetry-ext-http-requests/tests/coverage.xml \ No newline at end of file diff --git a/tox.ini b/tox.ini index 0db2364f197..268fe4e4879 100644 --- a/tox.ini +++ b/tox.ini @@ -15,6 +15,7 @@ python = [testenv] deps = mypy,mypyinstalled: mypy~=0.711 + test: coverage setenv = mypy: MYPYPATH={toxinidir}/opentelemetry-api/src/ @@ -47,7 +48,9 @@ commands_pre = mypyinstalled: pip install file://{toxinidir}/opentelemetry-api/ commands = - test: python -m unittest discover + test: coverage run --source {envsitepackagesdir}/opentelemetry -m unittest discover + test: coverage report + test: coverage xml mypy: mypy --namespace-packages opentelemetry-api/src/opentelemetry/ ; For test code, we don't want to enforce the full mypy strictness From f3af20fbbdd55ad7f24488a4cb9c4fab7668a955 Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Wed, 9 Oct 2019 13:15:43 -0700 Subject: [PATCH 08/20] Revert latest commit --- .codecov.yml | 5 ----- .gitignore | 1 - .travis.yml | 8 -------- tox.ini | 5 +---- 4 files changed, 1 insertion(+), 18 deletions(-) delete mode 100644 .codecov.yml diff --git a/.codecov.yml b/.codecov.yml deleted file mode 100644 index 77194778197..00000000000 --- a/.codecov.yml +++ /dev/null @@ -1,5 +0,0 @@ -fixes: - - "^.*/site-packages/opentelemetry/sdk/::opentelemetry-sdk/src/opentelemetry/sdk/" - - "^.*/site-packages/opentelemetry/ext/wsgi/::ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/" - - "^.*/site-packages/opentelemetry/ext/http_requests/::ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/" - - "^.*/site-packages/opentelemetry/::opentelemetry-api/src/opentelemetry/" diff --git a/.gitignore b/.gitignore index 9a72cbf8cef..679b6fd0ccf 100644 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,6 @@ pip-log.txt .tox .cache htmlcov -coverage.xml # Translations *.mo diff --git a/.travis.yml b/.travis.yml index 6e5c9e2039c..64eebc36213 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,11 +19,3 @@ install: script: - tox - -after_success: - - pip install codecov - - codecov -v --file - opentelemetry-api/tests/coverage.xml - opentelemetry-sdk/tests/coverage.xml - ext/opentelemetry-ext-wsgi/tests/coverage.xml - ext/opentelemetry-ext-http-requests/tests/coverage.xml \ No newline at end of file diff --git a/tox.ini b/tox.ini index 268fe4e4879..0db2364f197 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,6 @@ python = [testenv] deps = mypy,mypyinstalled: mypy~=0.711 - test: coverage setenv = mypy: MYPYPATH={toxinidir}/opentelemetry-api/src/ @@ -48,9 +47,7 @@ commands_pre = mypyinstalled: pip install file://{toxinidir}/opentelemetry-api/ commands = - test: coverage run --source {envsitepackagesdir}/opentelemetry -m unittest discover - test: coverage report - test: coverage xml + test: python -m unittest discover mypy: mypy --namespace-packages opentelemetry-api/src/opentelemetry/ ; For test code, we don't want to enforce the full mypy strictness From 0670790972b2938423a9aadb98114779236d85d3 Mon Sep 17 00:00:00 2001 From: Hector Hernandez Date: Wed, 16 Oct 2019 22:11:31 -0700 Subject: [PATCH 09/20] Initial commit --- ext/opentelemetry-ext-pymongo/README.rst | 21 +++ ext/opentelemetry-ext-pymongo/setup.cfg | 46 ++++++ ext/opentelemetry-ext-pymongo/setup.py | 26 +++ .../src/opentelemetry/ext/pymongo/__init__.py | 74 +++++++++ .../src/opentelemetry/ext/pymongo/version.py | 15 ++ .../tests/__init__.py | 0 .../tests/test_pymongo_integration.py | 156 ++++++++++++++++++ 7 files changed, 338 insertions(+) create mode 100644 ext/opentelemetry-ext-pymongo/README.rst create mode 100644 ext/opentelemetry-ext-pymongo/setup.cfg create mode 100644 ext/opentelemetry-ext-pymongo/setup.py create mode 100644 ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py create mode 100644 ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py create mode 100644 ext/opentelemetry-ext-pymongo/tests/__init__.py create mode 100644 ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py diff --git a/ext/opentelemetry-ext-pymongo/README.rst b/ext/opentelemetry-ext-pymongo/README.rst new file mode 100644 index 00000000000..9bd16d0673f --- /dev/null +++ b/ext/opentelemetry-ext-pymongo/README.rst @@ -0,0 +1,21 @@ +OpenTelemetry pymongo integration +================================= + +The integration with MongoDB supports the `pymongo`_ library and is specified +to ``trace_integrations`` using ``'pymongo'``. + +.. _pymongo: https://pypi.org/project/pymongo + +Usage +----- + +.. code:: python + + from opencensus.trace import config_integration + + config_integration.trace_integrations(['pymongo']) + +References +---------- + +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-pymongo/setup.cfg b/ext/opentelemetry-ext-pymongo/setup.cfg new file mode 100644 index 00000000000..821dfd1b0b9 --- /dev/null +++ b/ext/opentelemetry-ext-pymongo/setup.cfg @@ -0,0 +1,46 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-ext-pymongo +description = OpenTelemetry pymongo integration +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-pymongo +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 3 - Alpha + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api >= 0.1.dev0 + pymongo >= 3.1.0 + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-ext-pymongo/setup.py b/ext/opentelemetry-ext-pymongo/setup.py new file mode 100644 index 00000000000..ed63ddf42da --- /dev/null +++ b/ext/opentelemetry-ext-pymongo/setup.py @@ -0,0 +1,26 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "ext", "pymongo", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py new file mode 100644 index 00000000000..f2b328e8fc4 --- /dev/null +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py @@ -0,0 +1,74 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +The opentelemetry-ext-pymongo package allows tracing commands made by the +pymongo library. +""" + +from pymongo import monitoring + +from opentelemetry.trace import SpanKind, Span +from opentelemetry.trace.status import Status, StatusCanonicalCode + +MODULE_NAME = 'pymongo' +DATA_BASE_TYPE = 'mongodb' + +COMMAND_ATTRIBUTES = ['filter', 'sort', 'skip', 'limit', 'pipeline'] + + +def trace_integration(tracer=None): + """Integrate with pymongo to trace it using event listener. + https://api.mongodb.com/python/current/api/pymongo/monitoring.html + """ + monitoring.register(CommandTracer(tracer=tracer)) + + +class CommandTracer(monitoring.CommandListener): + def __init__(self, tracer=None): + self._tracer = tracer + + def started(self, event): + name: MODULE_NAME + "."+ event.command.get(event.command_name) + + with self._tracer.start_span(name, kind=SpanKind.CLIENT) as span: + span.set_attribute("component", DATA_BASE_TYPE) + span.set_attribute("db.type", DATA_BASE_TYPE) + span.set_attribute("db.instance", event.database_name) + span.set_attribute("db.statement", event.command.get(event.command_name)) + span.set_attribute("peer.address", str(event.connection_id)) + span.set_attribute("peer.hostname", str(event.connection_id)) #TODO: calculate this + span.set_attribute("peer.port", str(event.connection_id)) #TODO: calculate this + span.set_attribute("operation_id", event.operation_id) + + for attr in COMMAND_ATTRIBUTES: + _attr = event.command.get(attr) + if _attr is not None: + span.set_attribute(attr, str(_attr)) + + def succeeded(self, event): + self._endSpan(event, True) + + def failed(self, event): + self._endSpan(event, False) + + def _endSpan(self, event, succeeded): + span = self._tracer.current_span() + span.set_attribute("request_id", event.request_id) + span.set_attribute("duration_micros", event.duration_micros) + if succeeded: + span.set_status(Status(StatusCanonicalCode.OK, event.reply)) + else: + span.set_status(Status(StatusCanonicalCode.UNKNOWN, event.failure)) + self._tracer.end_span() diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py new file mode 100644 index 00000000000..a457c2b6651 --- /dev/null +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py @@ -0,0 +1,15 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.1.dev0" diff --git a/ext/opentelemetry-ext-pymongo/tests/__init__.py b/ext/opentelemetry-ext-pymongo/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py b/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py new file mode 100644 index 00000000000..8b215e95157 --- /dev/null +++ b/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py @@ -0,0 +1,156 @@ +# Copyright 2019, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest import mock + +from opentelemetry.ext.pymongo import trace_integration, CommandTracer + + +class TestPymongoIntegration(unittest.TestCase): + + def test_trace_integration(self): + mock_register = mock.Mock() + + patch = mock.patch( + 'pymongo.monitoring.register', + side_effect=mock_register) + + with patch: + trace_integration() + + self.assertTrue(mock_register.called) + + def test_started(self): + mock_tracer = MockTracer() + + patch = mock.patch( + 'opencensus.trace.execution_context.get_opencensus_tracer', + return_value=mock_tracer) + + command_attrs = { + 'filter': 'filter', + 'sort': 'sort', + 'limit': 'limit', + 'pipeline': 'pipeline', + 'command_name': 'find' + } + + expected_attrs = { + 'component': 'mongodb', + 'db.type': 'mongodb', + 'db.instance': 'database_name', + 'db.statement': 'find', + 'filter': 'filter', + 'sort': 'sort', + 'limit': 'limit', + 'pipeline': 'pipeline', + 'request_id': 'request_id', + 'connection_id': 'connection_id' + } + + expected_name = 'pymongo.database_name.find.command_name' + + with patch: + CommandTracer().started( + event=MockEvent(command_attrs)) + + self.assertEqual(mock_tracer.span.attributes, expected_attrs) + self.assertEqual(mock_tracer.span.name, expected_name) + + def test_succeed(self): + mock_tracer = MockTracer() + mock_tracer.start_span() + + patch = mock.patch( + 'opencensus.trace.execution_context.get_opencensus_tracer', + return_value=mock_tracer) + + expected_status = { + 'code': 0, + 'message': '', + 'details': None + } + + with patch: + CommandTracer().succeeded(event=MockEvent(None)) + + self.assertEqual(mock_tracer.span.status, expected_status) + mock_tracer.end_span.assert_called_with() + + def test_failed(self): + mock_tracer = MockTracer() + mock_tracer.start_span() + + patch = mock.patch( + 'opencensus.trace.execution_context.get_opencensus_tracer', + return_value=mock_tracer) + + expected_status = { + 'code': 2, + 'message': 'MongoDB error', + 'details': 'failure' + } + + with patch: + CommandTracer().failed(event=MockEvent(None)) + + self.assertEqual(mock_tracer.span.status, expected_status) + mock_tracer.end_span.assert_called_with() + + +class MockCommand(object): + def __init__(self, command_attrs): + self.command_attrs = command_attrs + + def get(self, key): + return self.command_attrs.get(key) + + +class MockEvent(object): + def __init__(self, command_attrs): + self.command = MockCommand(command_attrs) + + def __getattr__(self, item): + return item + + +class MockSpan(object): + def __init__(self): + self.status = None + + def set_status(self, status): + self.status = { + 'code': status.canonical_code, + 'message': status.description, + 'details': status.details, + } + + +class MockTracer(object): + def __init__(self): + self.span = MockSpan() + self.end_span = mock.Mock() + + def start_span(self, name=None): + self.span.name = name + self.span.attributes = {} + self.span.status = {} + return self.span + + def add_attribute_to_current_span(self, key, value): + self.span.attributes[key] = value + + def get_current_span(self): + return self.span From dbe5c6a814dd7aaf037269bedcb19cc7aaf7c692 Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Mon, 21 Oct 2019 13:51:14 -0700 Subject: [PATCH 10/20] Initial version --- ext/opentelemetry-ext-pymongo/README.rst | 12 +- .../src/opentelemetry/ext/pymongo/__init__.py | 60 ++++--- .../tests/test_pymongo_integration.py | 160 ++++++++---------- tox.ini | 17 ++ 4 files changed, 133 insertions(+), 116 deletions(-) diff --git a/ext/opentelemetry-ext-pymongo/README.rst b/ext/opentelemetry-ext-pymongo/README.rst index 9bd16d0673f..1e8011f4c22 100644 --- a/ext/opentelemetry-ext-pymongo/README.rst +++ b/ext/opentelemetry-ext-pymongo/README.rst @@ -2,7 +2,7 @@ OpenTelemetry pymongo integration ================================= The integration with MongoDB supports the `pymongo`_ library and is specified -to ``trace_integrations`` using ``'pymongo'``. +to ``trace_integration`` using ``'pymongo'``. .. _pymongo: https://pypi.org/project/pymongo @@ -11,9 +11,15 @@ Usage .. code:: python - from opencensus.trace import config_integration + from pymongo import MongoClient + from opentelemetry.trace import tracer + from opentelemetry.trace.ext.pymongo import trace_integration - config_integration.trace_integrations(['pymongo']) + trace_integration(tracer()) + client = MongoClient() + db = client["MongoDB_Database"] + collection = db["MongoDB_Collection"] + collection.find_one() References ---------- diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py index f2b328e8fc4..088f89c7bd2 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py @@ -18,57 +18,63 @@ """ from pymongo import monitoring - from opentelemetry.trace import SpanKind, Span from opentelemetry.trace.status import Status, StatusCanonicalCode -MODULE_NAME = 'pymongo' -DATA_BASE_TYPE = 'mongodb' - -COMMAND_ATTRIBUTES = ['filter', 'sort', 'skip', 'limit', 'pipeline'] +DATA_BASE_TYPE = "mongodb" +COMMAND_ATTRIBUTES = ["filter", "sort", "skip", "limit", "pipeline"] def trace_integration(tracer=None): """Integrate with pymongo to trace it using event listener. https://api.mongodb.com/python/current/api/pymongo/monitoring.html """ - monitoring.register(CommandTracer(tracer=tracer)) + monitoring.register(CommandTracer(tracer)) class CommandTracer(monitoring.CommandListener): - def __init__(self, tracer=None): + def __init__(self, tracer): self._tracer = tracer - def started(self, event): - name: MODULE_NAME + "."+ event.command.get(event.command_name) - + def started(self, event: monitoring.CommandStartedEvent): + name = ( + DATA_BASE_TYPE + + "." + + event.command_name + + "." + + event.command.get(event.command_name) + ) with self._tracer.start_span(name, kind=SpanKind.CLIENT) as span: span.set_attribute("component", DATA_BASE_TYPE) span.set_attribute("db.type", DATA_BASE_TYPE) span.set_attribute("db.instance", event.database_name) - span.set_attribute("db.statement", event.command.get(event.command_name)) - span.set_attribute("peer.address", str(event.connection_id)) - span.set_attribute("peer.hostname", str(event.connection_id)) #TODO: calculate this - span.set_attribute("peer.port", str(event.connection_id)) #TODO: calculate this + span.set_attribute( + "db.statement", + event.command_name + + " " + + event.command.get(event.command_name), + ) + if event.connection_id is not None: + span.set_attribute("peer.address", str(event.connection_id)) + span.set_attribute("peer.hostname", event.connection_id[0]) + span.set_attribute("peer.port", event.connection_id[1]) + span.set_attribute("operation_id", event.operation_id) + span.set_attribute("request_id", event.request_id) for attr in COMMAND_ATTRIBUTES: _attr = event.command.get(attr) if _attr is not None: span.set_attribute(attr, str(_attr)) - - def succeeded(self, event): - self._endSpan(event, True) - def failed(self, event): - self._endSpan(event, False) + def succeeded(self, event: monitoring.CommandSucceededEvent): + span = self._tracer.get_current_span() + span.set_attribute("duration_micros", event.duration_micros) + span.set_status(Status(StatusCanonicalCode.OK, event.reply)) + span.end() - def _endSpan(self, event, succeeded): - span = self._tracer.current_span() - span.set_attribute("request_id", event.request_id) + def failed(self, event: monitoring.CommandFailedEvent): + span = self._tracer.get_current_span() span.set_attribute("duration_micros", event.duration_micros) - if succeeded: - span.set_status(Status(StatusCanonicalCode.OK, event.reply)) - else: - span.set_status(Status(StatusCanonicalCode.UNKNOWN, event.failure)) - self._tracer.end_span() + span.set_status(Status(StatusCanonicalCode.UNKNOWN, event.failure)) + span.end() diff --git a/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py b/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py index 8b215e95157..fa5d2716fbc 100644 --- a/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py +++ b/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py @@ -15,99 +15,76 @@ import unittest from unittest import mock +from opentelemetry import trace as trace_api from opentelemetry.ext.pymongo import trace_integration, CommandTracer +from opentelemetry.util import time_ns class TestPymongoIntegration(unittest.TestCase): - def test_trace_integration(self): mock_register = mock.Mock() - patch = mock.patch( - 'pymongo.monitoring.register', - side_effect=mock_register) - + "pymongo.monitoring.register", side_effect=mock_register + ) with patch: trace_integration() self.assertTrue(mock_register.called) def test_started(self): - mock_tracer = MockTracer() - - patch = mock.patch( - 'opencensus.trace.execution_context.get_opencensus_tracer', - return_value=mock_tracer) - command_attrs = { - 'filter': 'filter', - 'sort': 'sort', - 'limit': 'limit', - 'pipeline': 'pipeline', - 'command_name': 'find' + "filter": "filter", + "sort": "sort", + "limit": "limit", + "pipeline": "pipeline", + "command_name": "find", } - - expected_attrs = { - 'component': 'mongodb', - 'db.type': 'mongodb', - 'db.instance': 'database_name', - 'db.statement': 'find', - 'filter': 'filter', - 'sort': 'sort', - 'limit': 'limit', - 'pipeline': 'pipeline', - 'request_id': 'request_id', - 'connection_id': 'connection_id' - } - - expected_name = 'pymongo.database_name.find.command_name' - - with patch: - CommandTracer().started( - event=MockEvent(command_attrs)) - - self.assertEqual(mock_tracer.span.attributes, expected_attrs) - self.assertEqual(mock_tracer.span.name, expected_name) - - def test_succeed(self): mock_tracer = MockTracer() - mock_tracer.start_span() - - patch = mock.patch( - 'opencensus.trace.execution_context.get_opencensus_tracer', - return_value=mock_tracer) - - expected_status = { - 'code': 0, - 'message': '', - 'details': None - } - - with patch: - CommandTracer().succeeded(event=MockEvent(None)) - - self.assertEqual(mock_tracer.span.status, expected_status) - mock_tracer.end_span.assert_called_with() + CommandTracer(mock_tracer).started( + event=MockEvent(command_attrs, ("test.com","1234")) + ) + span = mock_tracer.get_current_span() + self.assertIs(span.kind, trace_api.SpanKind.CLIENT) + self.assertEqual(span.name, "mongodb.command_name.find") + self.assertEqual(span.attributes["component"], "mongodb") + self.assertEqual(span.attributes["db.type"], "mongodb") + self.assertEqual(span.attributes["db.instance"], "database_name") + self.assertEqual(span.attributes["db.statement"], "command_name find") + self.assertEqual( + span.attributes["peer.address"], "('test.com', '1234')" + ) + self.assertEqual(span.attributes["peer.hostname"], "test.com") + self.assertEqual(span.attributes["peer.port"], "1234") + self.assertEqual(span.attributes["operation_id"], "operation_id") + self.assertEqual(span.attributes["request_id"], "request_id") + + self.assertEqual(span.attributes["filter"], "filter") + self.assertEqual(span.attributes["sort"], "sort") + self.assertEqual(span.attributes["limit"], "limit") + self.assertEqual(span.attributes["pipeline"], "pipeline") + + def test_succeeded(self): + mock_tracer = MockTracer() + CommandTracer(mock_tracer).succeeded(event=MockEvent(None)) + span = mock_tracer.get_current_span() + self.assertEqual(span.attributes["duration_micros"], "duration_micros") + self.assertIs( + span.status.canonical_code, trace_api.status.StatusCanonicalCode.OK + ) + self.assertEqual(span.status.description, "reply") + self.assertIsNotNone(span.end_time) def test_failed(self): mock_tracer = MockTracer() - mock_tracer.start_span() - - patch = mock.patch( - 'opencensus.trace.execution_context.get_opencensus_tracer', - return_value=mock_tracer) - - expected_status = { - 'code': 2, - 'message': 'MongoDB error', - 'details': 'failure' - } - - with patch: - CommandTracer().failed(event=MockEvent(None)) - - self.assertEqual(mock_tracer.span.status, expected_status) - mock_tracer.end_span.assert_called_with() + CommandTracer(mock_tracer).failed(event=MockEvent(None)) + span = mock_tracer.get_current_span() + self.assertEqual(span.attributes["duration_micros"], "duration_micros") + self.assertIs( + span.status.canonical_code, + trace_api.status.StatusCanonicalCode.UNKNOWN, + ) + self.assertEqual(span.status.description, "failure") + self.assertIsNotNone(span.end_time) class MockCommand(object): @@ -119,38 +96,49 @@ def get(self, key): class MockEvent(object): - def __init__(self, command_attrs): + def __init__(self, command_attrs, connection_id=""): self.command = MockCommand(command_attrs) + self.connection_id = connection_id def __getattr__(self, item): return item class MockSpan(object): + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + return False + def __init__(self): self.status = None + self.name = "" + self.kind = trace_api.SpanKind.INTERNAL + self.attributes = None + self.end_time = None + + def set_attribute(self, key, value): + self.attributes[key] = value def set_status(self, status): - self.status = { - 'code': status.canonical_code, - 'message': status.description, - 'details': status.details, - } + self.status = status + + def end(self, end_time=None): + self.end_time = end_time if end_time is not None else time_ns() class MockTracer(object): def __init__(self): self.span = MockSpan() self.end_span = mock.Mock() + self.span.attributes = {} + self.span.status = None - def start_span(self, name=None): + def start_span(self, name, kind): self.span.name = name - self.span.attributes = {} - self.span.status = {} + self.span.kind = kind return self.span - def add_attribute_to_current_span(self, key, value): - self.span.attributes[key] = value - def get_current_span(self): return self.span diff --git a/tox.ini b/tox.ini index 73507c3be3a..90658dba2b5 100644 --- a/tox.ini +++ b/tox.ini @@ -4,6 +4,8 @@ skip_missing_interpreters = True envlist = py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-http-requests} pypy3-test-{api,sdk,example-app,ext-wsgi,ext-http-requests} + py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger,ext-pymongo} + pypy3-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger,ext-pymongo} lint py37-mypy docs @@ -25,6 +27,8 @@ changedir = test-example-app: opentelemetry-example-app/tests test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests test-ext-http-requests: ext/opentelemetry-ext-http-requests/tests + test-ext-pymongo: ext/opentelemetry-ext-pymongo/tests + test-example-app: examples/opentelemetry-example-app/tests commands_pre = ; Install without -e to test the actual installation @@ -38,6 +42,13 @@ commands_pre = ext: pip install {toxinidir}/opentelemetry-api wsgi: pip install {toxinidir}/ext/opentelemetry-ext-wsgi http-requests: pip install {toxinidir}/ext/opentelemetry-ext-http-requests + pymongo: pip install {toxinidir}/ext/opentelemetry-ext-pymongo + jaeger: pip install {toxinidir}/opentelemetry-sdk + jaeger: pip install {toxinidir}/ext/opentelemetry-ext-jaeger + +; Using file:// here because otherwise tox invokes just "pip install +; opentelemetry-api", leading to an error + mypyinstalled: pip install file://{toxinidir}/opentelemetry-api/ commands = mypy: mypy --namespace-packages opentelemetry-api/src/opentelemetry/ @@ -59,6 +70,8 @@ commands_pre = pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi pip install -e {toxinidir}/ext/opentelemetry-ext-http-requests pip install -e {toxinidir}/opentelemetry-example-app + pip install -e {toxinidir}/ext/opentelemetry-ext-pymongo + pip install -e {toxinidir}/examples/opentelemetry-example-app commands = ; Prefer putting everything in one pylint command to profit from duplication @@ -73,6 +86,10 @@ commands = ext/opentelemetry-ext-wsgi/tests/ \ opentelemetry-example-app/src/opentelemetry_example_app/ \ opentelemetry-example-app/tests/ + ext/opentelemetry-ext-pymongo/src/ \ + ext/opentelemetry-ext-pymongo/tests/ \ + examples/opentelemetry-example-app/src/opentelemetry_example_app/ \ + examples/opentelemetry-example-app/tests/ flake8 . isort --check-only --diff --recursive . From 4db5db65712cc1757aa24239c55bbd5285c262ac Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Mon, 21 Oct 2019 17:01:02 -0700 Subject: [PATCH 11/20] Lint fix --- ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py b/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py index fa5d2716fbc..880bc3e1be9 100644 --- a/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py +++ b/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py @@ -41,7 +41,7 @@ def test_started(self): } mock_tracer = MockTracer() CommandTracer(mock_tracer).started( - event=MockEvent(command_attrs, ("test.com","1234")) + event=MockEvent(command_attrs, ("test.com", "1234")) ) span = mock_tracer.get_current_span() self.assertIs(span.kind, trace_api.SpanKind.CLIENT) From 5783c16269ece7b14eba678147edd2f4989db2cb Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Mon, 21 Oct 2019 17:03:32 -0700 Subject: [PATCH 12/20] Remove with to avoid span to be closed --- .../src/opentelemetry/ext/pymongo/__init__.py | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py index 088f89c7bd2..336a0b5b420 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py @@ -44,28 +44,26 @@ def started(self, event: monitoring.CommandStartedEvent): + "." + event.command.get(event.command_name) ) - with self._tracer.start_span(name, kind=SpanKind.CLIENT) as span: - span.set_attribute("component", DATA_BASE_TYPE) - span.set_attribute("db.type", DATA_BASE_TYPE) - span.set_attribute("db.instance", event.database_name) - span.set_attribute( - "db.statement", - event.command_name - + " " - + event.command.get(event.command_name), - ) - if event.connection_id is not None: - span.set_attribute("peer.address", str(event.connection_id)) - span.set_attribute("peer.hostname", event.connection_id[0]) - span.set_attribute("peer.port", event.connection_id[1]) + span = self._tracer.start_span(name, kind=SpanKind.CLIENT) + span.set_attribute("component", DATA_BASE_TYPE) + span.set_attribute("db.type", DATA_BASE_TYPE) + span.set_attribute("db.instance", event.database_name) + span.set_attribute( + "db.statement", + event.command_name + " " + event.command.get(event.command_name), + ) + if event.connection_id is not None: + span.set_attribute("peer.address", str(event.connection_id)) + span.set_attribute("peer.hostname", event.connection_id[0]) + span.set_attribute("peer.port", event.connection_id[1]) - span.set_attribute("operation_id", event.operation_id) - span.set_attribute("request_id", event.request_id) + span.set_attribute("operation_id", event.operation_id) + span.set_attribute("request_id", event.request_id) - for attr in COMMAND_ATTRIBUTES: - _attr = event.command.get(attr) - if _attr is not None: - span.set_attribute(attr, str(_attr)) + for attr in COMMAND_ATTRIBUTES: + _attr = event.command.get(attr) + if _attr is not None: + span.set_attribute(attr, str(_attr)) def succeeded(self, event: monitoring.CommandSucceededEvent): span = self._tracer.get_current_span() From bf6ec3b83ad49d78eb997022135b7b165aad2822 Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Tue, 22 Oct 2019 14:37:59 -0700 Subject: [PATCH 13/20] Addressing comments --- ext/opentelemetry-ext-pymongo/setup.cfg | 2 +- .../src/opentelemetry/ext/pymongo/__init__.py | 34 ++++++++----------- .../tests/test_pymongo_integration.py | 30 +++++++++------- tox.ini | 2 +- 4 files changed, 35 insertions(+), 33 deletions(-) diff --git a/ext/opentelemetry-ext-pymongo/setup.cfg b/ext/opentelemetry-ext-pymongo/setup.cfg index 821dfd1b0b9..3d99ca8a599 100644 --- a/ext/opentelemetry-ext-pymongo/setup.cfg +++ b/ext/opentelemetry-ext-pymongo/setup.cfg @@ -40,7 +40,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api >= 0.1.dev0 - pymongo >= 3.1.0 + pymongo ~= 3.1 [options.packages.find] where = src diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py index 336a0b5b420..a7e642d3373 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py @@ -21,7 +21,7 @@ from opentelemetry.trace import SpanKind, Span from opentelemetry.trace.status import Status, StatusCanonicalCode -DATA_BASE_TYPE = "mongodb" +DATABASE_TYPE = "mongodb" COMMAND_ATTRIBUTES = ["filter", "sort", "skip", "limit", "pipeline"] @@ -29,6 +29,7 @@ def trace_integration(tracer=None): """Integrate with pymongo to trace it using event listener. https://api.mongodb.com/python/current/api/pymongo/monitoring.html """ + monitoring.register(CommandTracer(tracer)) @@ -37,42 +38,37 @@ def __init__(self, tracer): self._tracer = tracer def started(self, event: monitoring.CommandStartedEvent): - name = ( - DATA_BASE_TYPE - + "." - + event.command_name - + "." - + event.command.get(event.command_name) - ) + command = event.command.get(event.command_name) + if command is None: + command = "" + name = DATABASE_TYPE + "." + event.command_name + "." + command span = self._tracer.start_span(name, kind=SpanKind.CLIENT) - span.set_attribute("component", DATA_BASE_TYPE) - span.set_attribute("db.type", DATA_BASE_TYPE) + span.set_attribute("component", DATABASE_TYPE) + span.set_attribute("db.type", DATABASE_TYPE) span.set_attribute("db.instance", event.database_name) - span.set_attribute( - "db.statement", - event.command_name + " " + event.command.get(event.command_name), - ) + span.set_attribute("db.statement", event.command_name + " " + command) if event.connection_id is not None: span.set_attribute("peer.address", str(event.connection_id)) span.set_attribute("peer.hostname", event.connection_id[0]) span.set_attribute("peer.port", event.connection_id[1]) - span.set_attribute("operation_id", event.operation_id) - span.set_attribute("request_id", event.request_id) + # pymongo specific, not specified by spec + span.set_attribute("db.mongo.operation_id", event.operation_id) + span.set_attribute("db.mongo.request_id", event.request_id) for attr in COMMAND_ATTRIBUTES: _attr = event.command.get(attr) if _attr is not None: - span.set_attribute(attr, str(_attr)) + span.set_attribute("db.mongo." + attr, str(_attr)) def succeeded(self, event: monitoring.CommandSucceededEvent): span = self._tracer.get_current_span() - span.set_attribute("duration_micros", event.duration_micros) + span.set_attribute("db.mongo.duration_micros", event.duration_micros) span.set_status(Status(StatusCanonicalCode.OK, event.reply)) span.end() def failed(self, event: monitoring.CommandFailedEvent): span = self._tracer.get_current_span() - span.set_attribute("duration_micros", event.duration_micros) + span.set_attribute("db.mongo.duration_micros", event.duration_micros) span.set_status(Status(StatusCanonicalCode.UNKNOWN, event.failure)) span.end() diff --git a/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py b/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py index 880bc3e1be9..2be77a2f8f3 100644 --- a/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py +++ b/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py @@ -55,19 +55,23 @@ def test_started(self): ) self.assertEqual(span.attributes["peer.hostname"], "test.com") self.assertEqual(span.attributes["peer.port"], "1234") - self.assertEqual(span.attributes["operation_id"], "operation_id") - self.assertEqual(span.attributes["request_id"], "request_id") + self.assertEqual( + span.attributes["db.mongo.operation_id"], "operation_id" + ) + self.assertEqual(span.attributes["db.mongo.request_id"], "request_id") - self.assertEqual(span.attributes["filter"], "filter") - self.assertEqual(span.attributes["sort"], "sort") - self.assertEqual(span.attributes["limit"], "limit") - self.assertEqual(span.attributes["pipeline"], "pipeline") + self.assertEqual(span.attributes["db.mongo.filter"], "filter") + self.assertEqual(span.attributes["db.mongo.sort"], "sort") + self.assertEqual(span.attributes["db.mongo.limit"], "limit") + self.assertEqual(span.attributes["db.mongo.pipeline"], "pipeline") def test_succeeded(self): mock_tracer = MockTracer() CommandTracer(mock_tracer).succeeded(event=MockEvent(None)) span = mock_tracer.get_current_span() - self.assertEqual(span.attributes["duration_micros"], "duration_micros") + self.assertEqual( + span.attributes["db.mongo.duration_micros"], "duration_micros" + ) self.assertIs( span.status.canonical_code, trace_api.status.StatusCanonicalCode.OK ) @@ -78,7 +82,9 @@ def test_failed(self): mock_tracer = MockTracer() CommandTracer(mock_tracer).failed(event=MockEvent(None)) span = mock_tracer.get_current_span() - self.assertEqual(span.attributes["duration_micros"], "duration_micros") + self.assertEqual( + span.attributes["db.mongo.duration_micros"], "duration_micros" + ) self.assertIs( span.status.canonical_code, trace_api.status.StatusCanonicalCode.UNKNOWN, @@ -87,7 +93,7 @@ def test_failed(self): self.assertIsNotNone(span.end_time) -class MockCommand(object): +class MockCommand(): def __init__(self, command_attrs): self.command_attrs = command_attrs @@ -95,7 +101,7 @@ def get(self, key): return self.command_attrs.get(key) -class MockEvent(object): +class MockEvent(): def __init__(self, command_attrs, connection_id=""): self.command = MockCommand(command_attrs) self.connection_id = connection_id @@ -104,7 +110,7 @@ def __getattr__(self, item): return item -class MockSpan(object): +class MockSpan(): def __enter__(self): return self @@ -128,7 +134,7 @@ def end(self, end_time=None): self.end_time = end_time if end_time is not None else time_ns() -class MockTracer(object): +class MockTracer(): def __init__(self): self.span = MockSpan() self.end_span = mock.Mock() diff --git a/tox.ini b/tox.ini index ad47fc32085..a8c45b6b69c 100644 --- a/tox.ini +++ b/tox.ini @@ -93,7 +93,7 @@ commands = ext/opentelemetry-ext-http-requests/tests/ \ ext/opentelemetry-ext-jaeger/src/opentelemetry \ ext/opentelemetry-ext-jaeger/tests/ \ - ext/opentelemetry-ext-pymongo/src/ \ + ext/opentelemetry-ext-pymongo/src/opentelemetry \ ext/opentelemetry-ext-pymongo/tests/ \ ext/opentelemetry-ext-wsgi/tests/ \ examples/opentelemetry-example-app/src/opentelemetry_example_app/ \ From 17dcda7b8435dba5be20bec523bf6acadf05be1b Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Tue, 22 Oct 2019 15:02:03 -0700 Subject: [PATCH 14/20] Reformat using black --- .../tests/test_pymongo_integration.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py b/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py index 2be77a2f8f3..f747b5f8462 100644 --- a/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py +++ b/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py @@ -93,7 +93,7 @@ def test_failed(self): self.assertIsNotNone(span.end_time) -class MockCommand(): +class MockCommand: def __init__(self, command_attrs): self.command_attrs = command_attrs @@ -101,7 +101,7 @@ def get(self, key): return self.command_attrs.get(key) -class MockEvent(): +class MockEvent: def __init__(self, command_attrs, connection_id=""): self.command = MockCommand(command_attrs) self.connection_id = connection_id @@ -110,7 +110,7 @@ def __getattr__(self, item): return item -class MockSpan(): +class MockSpan: def __enter__(self): return self @@ -134,7 +134,7 @@ def end(self, end_time=None): self.end_time = end_time if end_time is not None else time_ns() -class MockTracer(): +class MockTracer: def __init__(self): self.span = MockSpan() self.end_span = mock.Mock() From 40f2abd6f197bef4eebf99cf7c00ad6b3337673e Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Tue, 22 Oct 2019 15:17:51 -0700 Subject: [PATCH 15/20] Fixing lint issues --- .../src/opentelemetry/ext/pymongo/__init__.py | 3 ++- .../tests/test_pymongo_integration.py | 9 ++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py index a7e642d3373..0ceffe97ca8 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py @@ -18,7 +18,8 @@ """ from pymongo import monitoring -from opentelemetry.trace import SpanKind, Span + +from opentelemetry.trace import SpanKind from opentelemetry.trace.status import Status, StatusCanonicalCode DATABASE_TYPE = "mongodb" diff --git a/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py b/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py index f747b5f8462..81462602b95 100644 --- a/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py +++ b/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py @@ -13,17 +13,16 @@ # limitations under the License. import unittest -from unittest import mock from opentelemetry import trace as trace_api -from opentelemetry.ext.pymongo import trace_integration, CommandTracer +from opentelemetry.ext.pymongo import CommandTracer, trace_integration from opentelemetry.util import time_ns class TestPymongoIntegration(unittest.TestCase): def test_trace_integration(self): - mock_register = mock.Mock() - patch = mock.patch( + mock_register = unittest.mock.Mock() + patch = unittest.mock.patch( "pymongo.monitoring.register", side_effect=mock_register ) with patch: @@ -137,7 +136,7 @@ def end(self, end_time=None): class MockTracer: def __init__(self): self.span = MockSpan() - self.end_span = mock.Mock() + self.end_span = unittest.mock.Mock() self.span.attributes = {} self.span.status = None From 35957480d1daeefbe0a94be8fcd7ca81278f0c73 Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Wed, 23 Oct 2019 11:42:56 -0700 Subject: [PATCH 16/20] Adding try/except and removing peer.address --- .../src/opentelemetry/ext/pymongo/__init__.py | 38 +++++++++++-------- .../tests/test_pymongo_integration.py | 10 ++--- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py index 0ceffe97ca8..77779d3332d 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py @@ -43,24 +43,30 @@ def started(self, event: monitoring.CommandStartedEvent): if command is None: command = "" name = DATABASE_TYPE + "." + event.command_name + "." + command - span = self._tracer.start_span(name, kind=SpanKind.CLIENT) - span.set_attribute("component", DATABASE_TYPE) - span.set_attribute("db.type", DATABASE_TYPE) - span.set_attribute("db.instance", event.database_name) - span.set_attribute("db.statement", event.command_name + " " + command) - if event.connection_id is not None: - span.set_attribute("peer.address", str(event.connection_id)) - span.set_attribute("peer.hostname", event.connection_id[0]) - span.set_attribute("peer.port", event.connection_id[1]) + try: + span = self._tracer.start_span(name, kind=SpanKind.CLIENT) + span.set_attribute("component", DATABASE_TYPE) + span.set_attribute("db.type", DATABASE_TYPE) + span.set_attribute("db.instance", event.database_name) + span.set_attribute( + "db.statement", event.command_name + " " + command + ) + if event.connection_id is not None: + span.set_attribute("peer.hostname", event.connection_id[0]) + span.set_attribute("peer.port", event.connection_id[1]) - # pymongo specific, not specified by spec - span.set_attribute("db.mongo.operation_id", event.operation_id) - span.set_attribute("db.mongo.request_id", event.request_id) + # pymongo specific, not specified by spec + span.set_attribute("db.mongo.operation_id", event.operation_id) + span.set_attribute("db.mongo.request_id", event.request_id) - for attr in COMMAND_ATTRIBUTES: - _attr = event.command.get(attr) - if _attr is not None: - span.set_attribute("db.mongo." + attr, str(_attr)) + for attr in COMMAND_ATTRIBUTES: + _attr = event.command.get(attr) + if _attr is not None: + span.set_attribute("db.mongo." + attr, str(_attr)) + except Exception as ex: # noqa pylint: disable=broad-except + if span is not None: + span.set_status(Status(StatusCanonicalCode.INTERNAL, ex)) + span.end() def succeeded(self, event: monitoring.CommandSucceededEvent): span = self._tracer.get_current_span() diff --git a/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py b/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py index 81462602b95..c712136346e 100644 --- a/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py +++ b/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py @@ -13,6 +13,7 @@ # limitations under the License. import unittest +from unittest import mock from opentelemetry import trace as trace_api from opentelemetry.ext.pymongo import CommandTracer, trace_integration @@ -21,8 +22,8 @@ class TestPymongoIntegration(unittest.TestCase): def test_trace_integration(self): - mock_register = unittest.mock.Mock() - patch = unittest.mock.patch( + mock_register = mock.Mock() + patch = mock.patch( "pymongo.monitoring.register", side_effect=mock_register ) with patch: @@ -49,9 +50,6 @@ def test_started(self): self.assertEqual(span.attributes["db.type"], "mongodb") self.assertEqual(span.attributes["db.instance"], "database_name") self.assertEqual(span.attributes["db.statement"], "command_name find") - self.assertEqual( - span.attributes["peer.address"], "('test.com', '1234')" - ) self.assertEqual(span.attributes["peer.hostname"], "test.com") self.assertEqual(span.attributes["peer.port"], "1234") self.assertEqual( @@ -136,7 +134,7 @@ def end(self, end_time=None): class MockTracer: def __init__(self): self.span = MockSpan() - self.end_span = unittest.mock.Mock() + self.end_span = mock.Mock() self.span.attributes = {} self.span.status = None From 3f602d92b71171a71bf3d6b00964005ccc8eaa78 Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Fri, 25 Oct 2019 16:35:24 -0700 Subject: [PATCH 17/20] Adding reference for current span to avoid conflicts with asynchronous traces --- .../src/opentelemetry/ext/pymongo/__init__.py | 56 ++++++++++++------- .../tests/test_pymongo_integration.py | 28 ++++++---- 2 files changed, 53 insertions(+), 31 deletions(-) diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py index 77779d3332d..fc416ff3bb7 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py @@ -36,46 +36,60 @@ def trace_integration(tracer=None): class CommandTracer(monitoring.CommandListener): def __init__(self, tracer): + if tracer is None: + raise ValueError("The tracer is not provided.") self._tracer = tracer + self._span = None def started(self, event: monitoring.CommandStartedEvent): command = event.command.get(event.command_name) if command is None: command = "" name = DATABASE_TYPE + "." + event.command_name + "." + command + self._span = None try: - span = self._tracer.start_span(name, kind=SpanKind.CLIENT) - span.set_attribute("component", DATABASE_TYPE) - span.set_attribute("db.type", DATABASE_TYPE) - span.set_attribute("db.instance", event.database_name) - span.set_attribute( + self._span = self._tracer.start_span(name, kind=SpanKind.CLIENT) + self._span.set_attribute("component", DATABASE_TYPE) + self._span.set_attribute("db.type", DATABASE_TYPE) + self._span.set_attribute("db.instance", event.database_name) + self._span.set_attribute( "db.statement", event.command_name + " " + command ) if event.connection_id is not None: - span.set_attribute("peer.hostname", event.connection_id[0]) - span.set_attribute("peer.port", event.connection_id[1]) + self._span.set_attribute( + "peer.hostname", event.connection_id[0] + ) + self._span.set_attribute("peer.port", event.connection_id[1]) # pymongo specific, not specified by spec - span.set_attribute("db.mongo.operation_id", event.operation_id) - span.set_attribute("db.mongo.request_id", event.request_id) + self._span.set_attribute( + "db.mongo.operation_id", event.operation_id + ) + self._span.set_attribute("db.mongo.request_id", event.request_id) for attr in COMMAND_ATTRIBUTES: _attr = event.command.get(attr) if _attr is not None: - span.set_attribute("db.mongo." + attr, str(_attr)) + self._span.set_attribute("db.mongo." + attr, str(_attr)) except Exception as ex: # noqa pylint: disable=broad-except - if span is not None: - span.set_status(Status(StatusCanonicalCode.INTERNAL, ex)) - span.end() + if self._span is not None: + self._span.set_status(Status(StatusCanonicalCode.INTERNAL, ex)) + self._span.end() def succeeded(self, event: monitoring.CommandSucceededEvent): - span = self._tracer.get_current_span() - span.set_attribute("db.mongo.duration_micros", event.duration_micros) - span.set_status(Status(StatusCanonicalCode.OK, event.reply)) - span.end() + if self._span is not None: + self._span.set_attribute( + "db.mongo.duration_micros", event.duration_micros + ) + self._span.set_status(Status(StatusCanonicalCode.OK, event.reply)) + self._span.end() def failed(self, event: monitoring.CommandFailedEvent): - span = self._tracer.get_current_span() - span.set_attribute("db.mongo.duration_micros", event.duration_micros) - span.set_status(Status(StatusCanonicalCode.UNKNOWN, event.failure)) - span.end() + if self._span is not None: + self._span.set_attribute( + "db.mongo.duration_micros", event.duration_micros + ) + self._span.set_status( + Status(StatusCanonicalCode.UNKNOWN, event.failure) + ) + self._span.end() diff --git a/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py b/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py index c712136346e..d57c74243aa 100644 --- a/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py +++ b/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py @@ -26,8 +26,9 @@ def test_trace_integration(self): patch = mock.patch( "pymongo.monitoring.register", side_effect=mock_register ) + mock_tracer = MockTracer() with patch: - trace_integration() + trace_integration(mock_tracer) self.assertTrue(mock_register.called) @@ -40,10 +41,12 @@ def test_started(self): "command_name": "find", } mock_tracer = MockTracer() - CommandTracer(mock_tracer).started( + command_tracer = CommandTracer(mock_tracer) + command_tracer.started( event=MockEvent(command_attrs, ("test.com", "1234")) ) - span = mock_tracer.get_current_span() + # pylint: disable=protected-access + span = command_tracer._span self.assertIs(span.kind, trace_api.SpanKind.CLIENT) self.assertEqual(span.name, "mongodb.command_name.find") self.assertEqual(span.attributes["component"], "mongodb") @@ -64,8 +67,12 @@ def test_started(self): def test_succeeded(self): mock_tracer = MockTracer() - CommandTracer(mock_tracer).succeeded(event=MockEvent(None)) - span = mock_tracer.get_current_span() + mock_event = MockEvent({}) + command_tracer = CommandTracer(mock_tracer) + command_tracer.started(event=mock_event) + command_tracer.succeeded(event=mock_event) + # pylint: disable=protected-access + span = command_tracer._span self.assertEqual( span.attributes["db.mongo.duration_micros"], "duration_micros" ) @@ -77,8 +84,12 @@ def test_succeeded(self): def test_failed(self): mock_tracer = MockTracer() - CommandTracer(mock_tracer).failed(event=MockEvent(None)) - span = mock_tracer.get_current_span() + mock_event = MockEvent({}) + command_tracer = CommandTracer(mock_tracer) + command_tracer.started(event=mock_event) + command_tracer.failed(event=mock_event) + # pylint: disable=protected-access + span = command_tracer._span self.assertEqual( span.attributes["db.mongo.duration_micros"], "duration_micros" ) @@ -142,6 +153,3 @@ def start_span(self, name, kind): self.span.name = name self.span.kind = kind return self.span - - def get_current_span(self): - return self.span From ee8adf2b8d2fe4e7b072a84ca88a85a91473c838 Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Wed, 30 Oct 2019 12:25:22 -0700 Subject: [PATCH 18/20] Addressing comments --- .../src/opentelemetry/ext/pymongo/__init__.py | 71 +++++++++++-------- .../tests/test_pymongo_integration.py | 62 ++++++++++++---- 2 files changed, 89 insertions(+), 44 deletions(-) diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py index fc416ff3bb7..72b7651b790 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py @@ -39,57 +39,70 @@ def __init__(self, tracer): if tracer is None: raise ValueError("The tracer is not provided.") self._tracer = tracer - self._span = None + self._span_dict = {} def started(self, event: monitoring.CommandStartedEvent): command = event.command.get(event.command_name) if command is None: command = "" name = DATABASE_TYPE + "." + event.command_name + "." + command - self._span = None try: - self._span = self._tracer.start_span(name, kind=SpanKind.CLIENT) - self._span.set_attribute("component", DATABASE_TYPE) - self._span.set_attribute("db.type", DATABASE_TYPE) - self._span.set_attribute("db.instance", event.database_name) - self._span.set_attribute( + span = self._tracer.start_span(name, kind=SpanKind.CLIENT) + span.set_attribute("component", DATABASE_TYPE) + span.set_attribute("db.type", DATABASE_TYPE) + span.set_attribute("db.instance", event.database_name) + span.set_attribute( "db.statement", event.command_name + " " + command ) if event.connection_id is not None: - self._span.set_attribute( - "peer.hostname", event.connection_id[0] - ) - self._span.set_attribute("peer.port", event.connection_id[1]) + span.set_attribute("peer.hostname", event.connection_id[0]) + span.set_attribute("peer.port", event.connection_id[1]) # pymongo specific, not specified by spec - self._span.set_attribute( - "db.mongo.operation_id", event.operation_id - ) - self._span.set_attribute("db.mongo.request_id", event.request_id) + span.set_attribute("db.mongo.operation_id", event.operation_id) + span.set_attribute("db.mongo.request_id", event.request_id) for attr in COMMAND_ATTRIBUTES: _attr = event.command.get(attr) if _attr is not None: - self._span.set_attribute("db.mongo." + attr, str(_attr)) + span.set_attribute("db.mongo." + attr, str(_attr)) + + # Add Span to dictionary + self._span_dict[_get_span_dict_key(event)] = span except Exception as ex: # noqa pylint: disable=broad-except - if self._span is not None: - self._span.set_status(Status(StatusCanonicalCode.INTERNAL, ex)) - self._span.end() + if span is not None: + span.set_status(Status(StatusCanonicalCode.INTERNAL, ex)) + span.end() + self._remove_span(event) def succeeded(self, event: monitoring.CommandSucceededEvent): - if self._span is not None: - self._span.set_attribute( + span = self._span_dict[_get_span_dict_key(event)] + if span is not None: + span.set_attribute( "db.mongo.duration_micros", event.duration_micros ) - self._span.set_status(Status(StatusCanonicalCode.OK, event.reply)) - self._span.end() + span.set_status(Status(StatusCanonicalCode.OK, event.reply)) + span.end() + self._remove_span(event) def failed(self, event: monitoring.CommandFailedEvent): - if self._span is not None: - self._span.set_attribute( + span = self._span_dict[_get_span_dict_key(event)] + if span is not None: + span.set_attribute( "db.mongo.duration_micros", event.duration_micros ) - self._span.set_status( - Status(StatusCanonicalCode.UNKNOWN, event.failure) - ) - self._span.end() + span.set_status(Status(StatusCanonicalCode.UNKNOWN, event.failure)) + span.end() + self._remove_span(event) + + def _get_span(self, event): + return self._span_dict[_get_span_dict_key(event)] + + def _remove_span(self, event): + self._span_dict.pop(_get_span_dict_key(event)) + + +def _get_span_dict_key(event): + if event.connection_id is not None: + return (event.request_id, str(event.connection_id)) + return event.request_id diff --git a/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py b/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py index d57c74243aa..2678a5f8e60 100644 --- a/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py +++ b/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py @@ -42,11 +42,12 @@ def test_started(self): } mock_tracer = MockTracer() command_tracer = CommandTracer(mock_tracer) - command_tracer.started( - event=MockEvent(command_attrs, ("test.com", "1234")) + mock_event = MockEvent( + command_attrs, ("test.com", "1234"), "test_request_id" ) + command_tracer.started(event=mock_event) # pylint: disable=protected-access - span = command_tracer._span + span = command_tracer._get_span(mock_event) self.assertIs(span.kind, trace_api.SpanKind.CLIENT) self.assertEqual(span.name, "mongodb.command_name.find") self.assertEqual(span.attributes["component"], "mongodb") @@ -58,7 +59,9 @@ def test_started(self): self.assertEqual( span.attributes["db.mongo.operation_id"], "operation_id" ) - self.assertEqual(span.attributes["db.mongo.request_id"], "request_id") + self.assertEqual( + span.attributes["db.mongo.request_id"], "test_request_id" + ) self.assertEqual(span.attributes["db.mongo.filter"], "filter") self.assertEqual(span.attributes["db.mongo.sort"], "sort") @@ -70,9 +73,9 @@ def test_succeeded(self): mock_event = MockEvent({}) command_tracer = CommandTracer(mock_tracer) command_tracer.started(event=mock_event) - command_tracer.succeeded(event=mock_event) # pylint: disable=protected-access - span = command_tracer._span + span = command_tracer._get_span(mock_event) + command_tracer.succeeded(event=mock_event) self.assertEqual( span.attributes["db.mongo.duration_micros"], "duration_micros" ) @@ -87,9 +90,9 @@ def test_failed(self): mock_event = MockEvent({}) command_tracer = CommandTracer(mock_tracer) command_tracer.started(event=mock_event) - command_tracer.failed(event=mock_event) # pylint: disable=protected-access - span = command_tracer._span + span = command_tracer._get_span(mock_event) + command_tracer.failed(event=mock_event) self.assertEqual( span.attributes["db.mongo.duration_micros"], "duration_micros" ) @@ -100,6 +103,33 @@ def test_failed(self): self.assertEqual(span.status.description, "failure") self.assertIsNotNone(span.end_time) + def test_multiple_commands(self): + mock_tracer = MockTracer() + first_mock_event = MockEvent({}, ("firstUrl", "123"), "first") + second_mock_event = MockEvent({}, ("secondUrl", "456"), "second") + command_tracer = CommandTracer(mock_tracer) + command_tracer.started(event=first_mock_event) + # pylint: disable=protected-access + first_span = command_tracer._get_span(first_mock_event) + command_tracer.started(event=second_mock_event) + # pylint: disable=protected-access + second_span = command_tracer._get_span(second_mock_event) + command_tracer.succeeded(event=first_mock_event) + command_tracer.failed(event=second_mock_event) + + self.assertEqual(first_span.attributes["db.mongo.request_id"], "first") + self.assertIs( + first_span.status.canonical_code, + trace_api.status.StatusCanonicalCode.OK, + ) + self.assertEqual( + second_span.attributes["db.mongo.request_id"], "second" + ) + self.assertIs( + second_span.status.canonical_code, + trace_api.status.StatusCanonicalCode.UNKNOWN, + ) + class MockCommand: def __init__(self, command_attrs): @@ -110,9 +140,10 @@ def get(self, key): class MockEvent: - def __init__(self, command_attrs, connection_id=""): + def __init__(self, command_attrs, connection_id=None, request_id=""): self.command = MockCommand(command_attrs) self.connection_id = connection_id + self.request_id = request_id def __getattr__(self, item): return item @@ -144,12 +175,13 @@ def end(self, end_time=None): class MockTracer: def __init__(self): - self.span = MockSpan() self.end_span = mock.Mock() - self.span.attributes = {} - self.span.status = None + # pylint: disable=no-self-use def start_span(self, name, kind): - self.span.name = name - self.span.kind = kind - return self.span + span = MockSpan() + span.attributes = {} + span.status = None + span.name = name + span.kind = kind + return span From 2b233704a996a007982425ca8cd4584ca2bf7c27 Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Thu, 31 Oct 2019 11:24:07 -0700 Subject: [PATCH 19/20] Addressing comments --- .../src/opentelemetry/ext/pymongo/__init__.py | 25 ++++++++++--------- .../tests/test_pymongo_integration.py | 4 +-- tox.ini | 4 +-- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py index 72b7651b790..fa1cc1583e1 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/__init__.py @@ -42,18 +42,19 @@ def __init__(self, tracer): self._span_dict = {} def started(self, event: monitoring.CommandStartedEvent): - command = event.command.get(event.command_name) - if command is None: - command = "" - name = DATABASE_TYPE + "." + event.command_name + "." + command + command = event.command.get(event.command_name, "") + name = DATABASE_TYPE + "." + event.command_name + statement = event.command_name + if command: + name += "." + command + statement += " " + command + try: span = self._tracer.start_span(name, kind=SpanKind.CLIENT) span.set_attribute("component", DATABASE_TYPE) span.set_attribute("db.type", DATABASE_TYPE) span.set_attribute("db.instance", event.database_name) - span.set_attribute( - "db.statement", event.command_name + " " + command - ) + span.set_attribute("db.statement", statement) if event.connection_id is not None: span.set_attribute("peer.hostname", event.connection_id[0]) span.set_attribute("peer.port", event.connection_id[1]) @@ -71,12 +72,12 @@ def started(self, event: monitoring.CommandStartedEvent): self._span_dict[_get_span_dict_key(event)] = span except Exception as ex: # noqa pylint: disable=broad-except if span is not None: - span.set_status(Status(StatusCanonicalCode.INTERNAL, ex)) + span.set_status(Status(StatusCanonicalCode.INTERNAL, str(ex))) span.end() self._remove_span(event) def succeeded(self, event: monitoring.CommandSucceededEvent): - span = self._span_dict[_get_span_dict_key(event)] + span = self._get_span(event) if span is not None: span.set_attribute( "db.mongo.duration_micros", event.duration_micros @@ -86,7 +87,7 @@ def succeeded(self, event: monitoring.CommandSucceededEvent): self._remove_span(event) def failed(self, event: monitoring.CommandFailedEvent): - span = self._span_dict[_get_span_dict_key(event)] + span = self._get_span(event) if span is not None: span.set_attribute( "db.mongo.duration_micros", event.duration_micros @@ -96,7 +97,7 @@ def failed(self, event: monitoring.CommandFailedEvent): self._remove_span(event) def _get_span(self, event): - return self._span_dict[_get_span_dict_key(event)] + return self._span_dict.get(_get_span_dict_key(event)) def _remove_span(self, event): self._span_dict.pop(_get_span_dict_key(event)) @@ -104,5 +105,5 @@ def _remove_span(self, event): def _get_span_dict_key(event): if event.connection_id is not None: - return (event.request_id, str(event.connection_id)) + return (event.request_id, event.connection_id) return event.request_id diff --git a/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py b/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py index 2678a5f8e60..95f0ae3413a 100644 --- a/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py +++ b/ext/opentelemetry-ext-pymongo/tests/test_pymongo_integration.py @@ -135,8 +135,8 @@ class MockCommand: def __init__(self, command_attrs): self.command_attrs = command_attrs - def get(self, key): - return self.command_attrs.get(key) + def get(self, key, default=""): + return self.command_attrs.get(key, default) class MockEvent: diff --git a/tox.ini b/tox.ini index 870c7f4b4f5..40c11b34a11 100644 --- a/tox.ini +++ b/tox.ini @@ -2,8 +2,8 @@ skipsdist = True skip_missing_interpreters = True envlist = - py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger,opentracing-shim} - pypy3-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger,opentracing-shim} + py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger,ext-pymongo,opentracing-shim} + pypy3-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger,ext-pymongo,opentracing-shim} lint py37-{mypy,mypyinstalled} docs From f2c344eba52e418ea73e078a5c291b4a5e7bc3ce Mon Sep 17 00:00:00 2001 From: Chris Kleinknecht Date: Thu, 7 Nov 2019 14:46:10 -0800 Subject: [PATCH 20/20] s/0.1.dev0/0.3.dev0/ --- ext/opentelemetry-ext-pymongo/setup.cfg | 2 +- .../src/opentelemetry/ext/pymongo/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/opentelemetry-ext-pymongo/setup.cfg b/ext/opentelemetry-ext-pymongo/setup.cfg index 3d99ca8a599..f9362c75b35 100644 --- a/ext/opentelemetry-ext-pymongo/setup.cfg +++ b/ext/opentelemetry-ext-pymongo/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.1.dev0 + opentelemetry-api >= 0.3.dev0 pymongo ~= 3.1 [options.packages.find] diff --git a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py index a457c2b6651..93ef792d051 100644 --- a/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py +++ b/ext/opentelemetry-ext-pymongo/src/opentelemetry/ext/pymongo/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.1.dev0" +__version__ = "0.3.dev0"