From b9f477c2038eb035260500ca58348c2fe73db376 Mon Sep 17 00:00:00 2001 From: Peter Giacomo Lombardo Date: Mon, 11 May 2020 20:32:04 +0200 Subject: [PATCH 01/18] Code comment update --- instana/meter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instana/meter.py b/instana/meter.py index ab4da7e3..80ed1f9d 100644 --- a/instana/meter.py +++ b/instana/meter.py @@ -133,7 +133,7 @@ def start(self): """ This function can be called at first boot or after a fork. In either case, it will assure that the Meter is in a proper state (via reset()) and spawn a new background - thread to periodically report queued spans + thread to periodically report the metrics payload. Note that this will abandon any previous thread object that (in the case of an `os.fork()`) should no longer exist in the forked process. From 2f692f801d7611039a059a340282d11fd111e3eb Mon Sep 17 00:00:00 2001 From: Peter Giacomo Lombardo Date: Mon, 11 May 2020 20:32:36 +0200 Subject: [PATCH 02/18] Function docs and add get_spans_by_filter helper --- tests/helpers.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/tests/helpers.py b/tests/helpers.py index 57bda117..c697487e 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -64,8 +64,38 @@ def get_first_span_by_name(spans, name): return None -def get_span_by_filter(spans, filter): +def get_first_span_by_filter(spans, filter): + """ + Get the first span in that matches + + Example: + filter = lambda span: span.n == "tornado-server" and span.data["http"]["status"] == 301 + tornado_301_span = get_first_span_by_filter(spans, filter) + + @param spans: the list of spans to search + @param filter: the filter to search by + @return: Span or None if nothing matched + """ for span in spans: if filter(span) is True: return span return None + + +def get_spans_by_filter(spans, filter): + """ + Get all spans in that matches + + Example: + filter = lambda span: span.n == "tornado-server" and span.data["http"]["status"] == 301 + tornado_301_spans = get_spans_by_filter(spans, filter) + + @param spans: the list of spans to search + @param filter: the filter to search by + @return: list of spans + """ + results = [] + for span in spans: + if filter(span) is True: + results.append(span) + return results From 9dd4daad6c19044ff0ffff889d3a43ce85b8815f Mon Sep 17 00:00:00 2001 From: Peter Giacomo Lombardo Date: Mon, 11 May 2020 20:46:57 +0200 Subject: [PATCH 03/18] Update tests to follow helper name change --- tests/test_cassandra-driver.py | 2 +- tests/test_couchbase.py | 14 +++++++------- tests/test_tornado_server.py | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/test_cassandra-driver.py b/tests/test_cassandra-driver.py index 94160704..64504d80 100644 --- a/tests/test_cassandra-driver.py +++ b/tests/test_cassandra-driver.py @@ -5,7 +5,7 @@ import unittest from instana.singletons import tracer -from .helpers import testenv, get_first_span_by_name, get_span_by_filter +from .helpers import testenv, get_first_span_by_name, get_first_span_by_filter from cassandra.cluster import Cluster from cassandra import ConsistencyLevel diff --git a/tests/test_couchbase.py b/tests/test_couchbase.py index 8ab7eaf8..29a3c0d0 100644 --- a/tests/test_couchbase.py +++ b/tests/test_couchbase.py @@ -3,7 +3,7 @@ import unittest from instana.singletons import tracer -from .helpers import testenv, get_first_span_by_name, get_span_by_filter +from .helpers import testenv, get_first_span_by_name, get_first_span_by_filter from couchbase.admin import Admin from couchbase.cluster import Cluster @@ -698,11 +698,11 @@ def test_lock(self): self.assertEqual(test_span.data["sdk"]["name"], 'test') filter = lambda span: span.n == "couchbase" and span.data["couchbase"]["type"] == "lock" - cb_lock_span = get_span_by_filter(spans, filter) + cb_lock_span = get_first_span_by_filter(spans, filter) self.assertIsNotNone(cb_lock_span) filter = lambda span: span.n == "couchbase" and span.data["couchbase"]["type"] == "upsert" - cb_upsert_span = get_span_by_filter(spans, filter) + cb_upsert_span = get_first_span_by_filter(spans, filter) self.assertIsNotNone(cb_upsert_span) # Same traceId and parent relationship @@ -746,11 +746,11 @@ def test_lock_unlock(self): self.assertEqual(test_span.data["sdk"]["name"], 'test') filter = lambda span: span.n == "couchbase" and span.data["couchbase"]["type"] == "lock" - cb_lock_span = get_span_by_filter(spans, filter) + cb_lock_span = get_first_span_by_filter(spans, filter) self.assertIsNotNone(cb_lock_span) filter = lambda span: span.n == "couchbase" and span.data["couchbase"]["type"] == "unlock" - cb_unlock_span = get_span_by_filter(spans, filter) + cb_unlock_span = get_first_span_by_filter(spans, filter) self.assertIsNotNone(cb_unlock_span) # Same traceId and parent relationship @@ -796,11 +796,11 @@ def test_lock_unlock_muilti(self): self.assertEqual(test_span.data["sdk"]["name"], 'test') filter = lambda span: span.n == "couchbase" and span.data["couchbase"]["type"] == "lock_multi" - cb_lock_span = get_span_by_filter(spans, filter) + cb_lock_span = get_first_span_by_filter(spans, filter) self.assertIsNotNone(cb_lock_span) filter = lambda span: span.n == "couchbase" and span.data["couchbase"]["type"] == "unlock_multi" - cb_unlock_span = get_span_by_filter(spans, filter) + cb_unlock_span = get_first_span_by_filter(spans, filter) self.assertIsNotNone(cb_unlock_span) # Same traceId and parent relationship diff --git a/tests/test_tornado_server.py b/tests/test_tornado_server.py index 60c58cb0..487c6b0f 100644 --- a/tests/test_tornado_server.py +++ b/tests/test_tornado_server.py @@ -10,7 +10,7 @@ from instana.singletons import async_tracer, agent -from .helpers import testenv, get_first_span_by_name, get_span_by_filter +from .helpers import testenv, get_first_span_by_name, get_first_span_by_filter class TestTornadoServer(unittest.TestCase): @@ -178,9 +178,9 @@ async def test(): self.assertEqual(4, len(spans)) filter = lambda span: span.n == "tornado-server" and span.data["http"]["status"] == 301 - tornado_301_span = get_span_by_filter(spans, filter) + tornado_301_span = get_first_span_by_filter(spans, filter) filter = lambda span: span.n == "tornado-server" and span.data["http"]["status"] == 200 - tornado_span = get_span_by_filter(spans, filter) + tornado_span = get_first_span_by_filter(spans, filter) aiohttp_span = get_first_span_by_name(spans, "aiohttp-client") test_span = get_first_span_by_name(spans, "sdk") From 89281cd85be6985f7b3a5eb9d4d89e011f5e4b9a Mon Sep 17 00:00:00 2001 From: Peter Giacomo Lombardo Date: Mon, 11 May 2020 21:05:14 +0200 Subject: [PATCH 04/18] Initial gevent instrumentation --- instana/__init__.py | 1 + instana/instrumentation/gevent_inst.py | 29 ++++++++++++++++++++++++++ setup.py | 1 + 3 files changed, 31 insertions(+) create mode 100644 instana/instrumentation/gevent_inst.py diff --git a/instana/__init__.py b/instana/__init__.py index c9de43f1..569f483e 100644 --- a/instana/__init__.py +++ b/instana/__init__.py @@ -122,6 +122,7 @@ def boot_agent(): from .instrumentation import cassandra_inst from .instrumentation import couchbase_inst from .instrumentation import flask + from .instrumentation import gevent_inst from .instrumentation import grpcio from .instrumentation.tornado import client from .instrumentation.tornado import server diff --git a/instana/instrumentation/gevent_inst.py b/instana/instrumentation/gevent_inst.py new file mode 100644 index 00000000..bc2e29c1 --- /dev/null +++ b/instana/instrumentation/gevent_inst.py @@ -0,0 +1,29 @@ +from __future__ import absolute_import + +import sys +from ..log import logger +from ..singletons import agent, tracer + +try: + if 'gevent' in sys.modules: + logger.debug("Instrumenting gevent") + + import gevent + from opentracing.scope_managers.gevent import GeventScopeManager + + def spawn_callback(gr): + current_greenlet = gevent.getcurrent() + parent_scope = tracer.scope_manager.active + + # logger.debug("current_greenlet: %s", current_greenlet) + # logger.debug("other greenlet: %s", gr) + + if (type(current_greenlet) is gevent._greenlet.Greenlet) and parent_scope is not None: + parent_scope._finish_on_close = False + tracer._scope_manager._set_greenlet_scope(parent_scope, gr) + + logger.debug(" -> Updating tracer to use gevent based context management") + tracer._scope_manager = GeventScopeManager() + gevent.Greenlet.add_spawn_callback(spawn_callback) +except ImportError: + pass diff --git a/setup.py b/setup.py index 5540c022..43f46f2a 100644 --- a/setup.py +++ b/setup.py @@ -75,6 +75,7 @@ def check_setuptools(): 'nose>=1.0', 'flask>=0.12.2', 'grpcio>=1.18.0', + 'gevent>=1.4.0', 'lxml>=3.4', 'mock>=2.0.0', 'mysqlclient>=1.3.14;python_version>="3.5"', From aadb2bcb093b18feeebc308b01e90e363f025d6c Mon Sep 17 00:00:00 2001 From: Peter Giacomo Lombardo Date: Tue, 12 May 2020 13:59:51 +0200 Subject: [PATCH 05/18] Use cloned scopes to propagate context --- instana/instrumentation/gevent_inst.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/instana/instrumentation/gevent_inst.py b/instana/instrumentation/gevent_inst.py index bc2e29c1..42eff059 100644 --- a/instana/instrumentation/gevent_inst.py +++ b/instana/instrumentation/gevent_inst.py @@ -2,6 +2,7 @@ import sys from ..log import logger +from opentracing.scope_managers.gevent import _GeventScope from ..singletons import agent, tracer try: @@ -12,15 +13,13 @@ from opentracing.scope_managers.gevent import GeventScopeManager def spawn_callback(gr): - current_greenlet = gevent.getcurrent() parent_scope = tracer.scope_manager.active - - # logger.debug("current_greenlet: %s", current_greenlet) - # logger.debug("other greenlet: %s", gr) - - if (type(current_greenlet) is gevent._greenlet.Greenlet) and parent_scope is not None: - parent_scope._finish_on_close = False - tracer._scope_manager._set_greenlet_scope(parent_scope, gr) + if parent_scope is not None: + # New greenlet, new clean slate. Clone and make active in this new greenlet + # the currently active scope (but don't close this span on close - it's a + # clone/not the original) + parent_scope_clone = _GeventScope(parent_scope.manager, parent_scope.span, finish_on_close=False) + tracer._scope_manager._set_greenlet_scope(parent_scope_clone, gr) logger.debug(" -> Updating tracer to use gevent based context management") tracer._scope_manager = GeventScopeManager() From 43f285570254d9a85d8d00395825c3f569eb0104 Mon Sep 17 00:00:00 2001 From: Peter Giacomo Lombardo Date: Tue, 12 May 2020 14:05:49 +0200 Subject: [PATCH 06/18] Updated tests with dedicated gevent run --- .circleci/config.yml | 38 +++++++++++++ tests/__init__.py | 116 +++++++++++++++++++-------------------- tests/apps/flaskalino.py | 1 + tests/test_gevent.py | 79 ++++++++++++++++++++++++++ 4 files changed, 176 insertions(+), 58 deletions(-) create mode 100644 tests/test_gevent.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 3f18b7d6..ecf3c4fa 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -261,6 +261,44 @@ jobs: . venv/bin/activate nosetests -v tests/test_cassandra-driver.py:TestCassandra + gevent: + docker: + - image: circleci/python:3.6.8 + + working_directory: ~/repo + + steps: + - checkout + + # Download and cache dependencies + - restore_cache: + keys: + - v1-dependencies-{{ checksum "requirements.txt" }} + # fallback to using the latest cache if no exact match is found + - v1-dependencies- + + - run: + name: install dependencies + command: | + sudo apt-get update + sudo apt install lsb-release -y + python -m venv venv + . venv/bin/activate + pip install -U pip + python setup.py install_egg_info + pip install -r requirements.txt + pip install -r requirements-test.txt + + - save_cache: + paths: + - ./venv + key: v1-dependencies-{{ checksum "requirements.txt" }} + + - run: + name: run tests + command: | + . venv/bin/activate + GEVENT=1 nosetests -v tests/test_gevent workflows: version: 2 build: diff --git a/tests/__init__.py b/tests/__init__.py index 9a302514..751a1839 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,16 +1,18 @@ from __future__ import absolute_import import os + +os.environ["INSTANA_TEST"] = "true" + +if 'GEVENT_TEST' in os.environ: + from gevent import monkey + monkey.patch_all() + import sys import time import threading -import logging -from instana.log import logger from .apps.flaskalino import flask_server -os.environ["INSTANA_TEST"] = "true" -logger.setLevel(logging.DEBUG) - # Background Flask application # @@ -22,58 +24,56 @@ print("Starting background Flask app...") flask.start() - -if sys.version_info >= (3, 5, 3): - # Background RPC application - # - # Spawn the background RPC app that the tests will throw - # requests at. - import tests.apps.grpc_server - from .apps.grpc_server.stan_server import StanServicer - stan_servicer = StanServicer() - rpc_server_thread = threading.Thread(target=stan_servicer.start_server) - rpc_server_thread.daemon = True - rpc_server_thread.name = "Background RPC app" - print("Starting background RPC app...") - rpc_server_thread.start() - - -if sys.version_info < (3, 7, 0): - # Background Soap Server - from .apps.soapserver4132 import soapserver - - # Spawn our background Soap server that the tests will throw - # requests at. - soap = threading.Thread(target=soapserver.serve_forever) - soap.daemon = True - soap.name = "Background Soap server" - print("Starting background Soap server...") - soap.start() - - -if sys.version_info >= (3, 5, 3): - # Background aiohttp application - from .apps.app_aiohttp import run_server - - # Spawn our background aiohttp app that the tests will throw - # requests at. - aio_server = threading.Thread(target=run_server) - aio_server.daemon = True - aio_server.name = "Background aiohttp server" - print("Starting background aiohttp server...") - aio_server.start() - - -if sys.version_info >= (3, 5, 3): - # Background Tornado application - from .apps.tornado import run_server - - # Spawn our background Tornado app that the tests will throw - # requests at. - tornado_server = threading.Thread(target=run_server) - tornado_server.daemon = True - tornado_server.name = "Background Tornado server" - print("Starting background Tornado server...") - tornado_server.start() +if 'GEVENT_TEST' not in os.environ: + + if sys.version_info >= (3, 5, 3): + # Background RPC application + # + # Spawn the background RPC app that the tests will throw + # requests at. + import tests.apps.grpc_server + from .apps.grpc_server.stan_server import StanServicer + stan_servicer = StanServicer() + rpc_server_thread = threading.Thread(target=stan_servicer.start_server) + rpc_server_thread.daemon = True + rpc_server_thread.name = "Background RPC app" + print("Starting background RPC app...") + rpc_server_thread.start() + + if sys.version_info < (3, 7, 0): + # Background Soap Server + from .apps.soapserver4132 import soapserver + + # Spawn our background Soap server that the tests will throw + # requests at. + soap = threading.Thread(target=soapserver.serve_forever) + soap.daemon = True + soap.name = "Background Soap server" + print("Starting background Soap server...") + soap.start() + + if sys.version_info >= (3, 5, 3): + # Background aiohttp application + from .apps.app_aiohttp import run_server + + # Spawn our background aiohttp app that the tests will throw + # requests at. + aio_server = threading.Thread(target=run_server) + aio_server.daemon = True + aio_server.name = "Background aiohttp server" + print("Starting background aiohttp server...") + aio_server.start() + + if sys.version_info >= (3, 5, 3): + # Background Tornado application + from .apps.tornado import run_server + + # Spawn our background Tornado app that the tests will throw + # requests at. + tornado_server = threading.Thread(target=run_server) + tornado_server.daemon = True + tornado_server.name = "Background Tornado server" + print("Starting background Tornado server...") + tornado_server.start() time.sleep(1) diff --git a/tests/apps/flaskalino.py b/tests/apps/flaskalino.py index 0669ef8a..7f939d8b 100644 --- a/tests/apps/flaskalino.py +++ b/tests/apps/flaskalino.py @@ -141,4 +141,5 @@ def handle_invalid_usage(error): if __name__ == '__main__': + flask_server.request_queue_size = 20 flask_server.serve_forever() diff --git a/tests/test_gevent.py b/tests/test_gevent.py new file mode 100644 index 00000000..db8ca6b8 --- /dev/null +++ b/tests/test_gevent.py @@ -0,0 +1,79 @@ +from __future__ import absolute_import + +import gevent +import unittest +import urllib3 + +from instana.singletons import tracer +from instana.span import SDKSpan +from .helpers import testenv, get_spans_by_filter +from opentracing.scope_managers.gevent import GeventScopeManager + + +class TestGEvent(unittest.TestCase): + def setUp(self): + self.http = urllib3.HTTPConnectionPool('127.0.0.1', port=testenv["wsgi_port"], maxsize=20) + self.recorder = tracer.recorder + self.recorder.clear_spans() + tracer._scope_manager = GeventScopeManager() + + def tearDown(self): + """ Do nothing for now """ + pass + + def make_http_call(self): + return self.http.request('GET', testenv["wsgi_server"] + '/') + + def spawn_calls(self): + with tracer.start_active_span('spawn_calls'): + jobs = [] + jobs.append(gevent.spawn(self.make_http_call)) + jobs.append(gevent.spawn(self.make_http_call)) + jobs.append(gevent.spawn(self.make_http_call)) + gevent.joinall(jobs, timeout=2) + + def launch_gevent_chain(self): + with tracer.start_active_span('test'): + gevent.spawn(self.spawn_calls).join() + + def test_spawning(self): + gevent.spawn(self.launch_gevent_chain) + + gevent.sleep(2) + + spans = self.recorder.queued_spans() + + self.assertEqual(8, len(spans)) + + span_filter = lambda span: span.n == "sdk" \ + and span.data['sdk']['name'] == 'test' and span.p == None + test_spans = get_spans_by_filter(spans, span_filter) + self.assertIsNotNone(test_spans) + self.assertEqual(len(test_spans), 1) + + test_span = test_spans[0] + self.assertTrue(type(test_spans[0]) is SDKSpan) + + span_filter = lambda span: span.n == "sdk" \ + and span.data['sdk']['name'] == 'spawn_calls' and span.p == test_span.s + spawn_spans = get_spans_by_filter(spans, span_filter) + self.assertIsNotNone(spawn_spans) + self.assertEqual(len(spawn_spans), 1) + + spawn_span = spawn_spans[0] + self.assertTrue(type(spawn_spans[0]) is SDKSpan) + + span_filter = lambda span: span.n == "urllib3" + urllib3_spans = get_spans_by_filter(spans, span_filter) + + for urllib3_span in urllib3_spans: + # spans should all have the same test span parent + self.assertEqual(urllib3_span.t, spawn_span.t) + self.assertEqual(urllib3_span.p, spawn_span.s) + + # find the wsgi span generated from this urllib3 request + span_filter = lambda span: span.n == "wsgi" and span.p == urllib3_span.s + wsgi_spans = get_spans_by_filter(spans, span_filter) + self.assertIsNotNone(wsgi_spans) + self.assertEqual(len(wsgi_spans), 1) + From 9e539097be13261c1591fc6403567362dfd94bb8 Mon Sep 17 00:00:00 2001 From: Peter Giacomo Lombardo Date: Tue, 12 May 2020 14:11:24 +0200 Subject: [PATCH 07/18] Update test python versions and workflow jobs --- .circleci/config.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index ecf3c4fa..e643fa43 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -112,9 +112,9 @@ jobs: . venv/bin/activate python runtests.py - python36: + python38: docker: - - image: circleci/python:3.6.8 + - image: circleci/python:3.8.2 # Specify service dependencies here if necessary # CircleCI maintains a library of pre-built images @@ -263,7 +263,7 @@ jobs: gevent: docker: - - image: circleci/python:3.6.8 + - image: circleci/python:3.8.2 working_directory: ~/repo @@ -304,7 +304,7 @@ workflows: build: jobs: - python27 - - python35 - - python36 + - python38 - py27cassandra - py36cassandra + - gevent From 574f903cbe3a00b251c54889eb76e1e5e2f5e304 Mon Sep 17 00:00:00 2001 From: Peter Giacomo Lombardo Date: Tue, 12 May 2020 16:19:03 +0200 Subject: [PATCH 08/18] CircleCI config cleanup; Break tests out --- .circleci/config.yml | 172 ++----------------------------------------- runtests.py | 5 +- setup.py | 14 +++- 3 files changed, 22 insertions(+), 169 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e643fa43..b0f3d813 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,29 +7,15 @@ jobs: python27: docker: - image: circleci/python:2.7.15 - - # Specify service dependencies here if necessary - # CircleCI maintains a library of pre-built images - # documented at https://circleci.com/docs/2.0/circleci-images/ - image: circleci/postgres:9.6.5-alpine-ram - image: circleci/mariadb:10.1-ram - image: circleci/redis:5.0.4 - image: rabbitmq:3.5.4 - image: couchbase/server-sandbox:5.5.0 - image: circleci/mongo:4.2.3-ram - working_directory: ~/repo - steps: - checkout - - # Download and cache dependencies - - restore_cache: - keys: - - v1-dependencies-{{ checksum "requirements.txt" }} - # fallback to using the latest cache if no exact match is found - - v1-dependencies- - - run: name: install dependencies command: | @@ -46,66 +32,7 @@ jobs: . venv/bin/activate pip install -U pip python setup.py install_egg_info - pip install -r requirements-test.txt - - - save_cache: - paths: - - ./venv - key: v1-dependencies-{{ checksum "requirements.txt" }} - - - run: - name: run tests - command: | - . venv/bin/activate - python runtests.py - - python35: - docker: - - image: circleci/python:3.5.6 - - # Specify service dependencies here if necessary - # CircleCI maintains a library of pre-built images - # documented at https://circleci.com/docs/2.0/circleci-images/ - - image: circleci/postgres:9.6.5-alpine-ram - - image: circleci/mariadb:10-ram - - image: circleci/redis:5.0.4 - - image: rabbitmq:3.5.4 - - image: couchbase/server-sandbox:5.5.0 - - image: circleci/mongo:4.2.3-ram - - working_directory: ~/repo - - steps: - - checkout - - # Download and cache dependencies - - restore_cache: - keys: - - v1-dependencies-{{ checksum "requirements.txt" }} - # fallback to using the latest cache if no exact match is found - - v1-dependencies- - - - run: - name: install dependencies - command: | - sudo apt-get update - sudo apt install lsb-release -y - curl -O https://packages.couchbase.com/releases/couchbase-release/couchbase-release-1.0-6-amd64.deb - sudo dpkg -i ./couchbase-release-1.0-6-amd64.deb - sudo apt-get update - sudo apt install libcouchbase-dev -y - python -m venv venv - . venv/bin/activate - pip install -U pip - python setup.py install_egg_info - pip install -r requirements.txt - pip install -r requirements-test.txt - - - save_cache: - paths: - - ./venv - key: v1-dependencies-{{ checksum "requirements.txt" }} - + pip install -e '.[test]' - run: name: run tests command: | @@ -115,29 +42,15 @@ jobs: python38: docker: - image: circleci/python:3.8.2 - - # Specify service dependencies here if necessary - # CircleCI maintains a library of pre-built images - # documented at https://circleci.com/docs/2.0/circleci-images/ - image: circleci/postgres:9.6.5-alpine-ram - image: circleci/mariadb:10-ram - image: circleci/redis:5.0.4 - image: rabbitmq:3.5.4 - image: couchbase/server-sandbox:5.5.0 - image: circleci/mongo:4.2.3-ram - working_directory: ~/repo - steps: - checkout - - # Download and cache dependencies - - restore_cache: - keys: - - v1-dependencies-{{ checksum "requirements.txt" }} - # fallback to using the latest cache if no exact match is found - - v1-dependencies- - - run: name: install dependencies command: | @@ -151,14 +64,7 @@ jobs: . venv/bin/activate pip install -U pip python setup.py install_egg_info - pip install -r requirements.txt - pip install -r requirements-test.txt - - - save_cache: - paths: - - ./venv - key: v1-dependencies-{{ checksum "requirements.txt" }} - + pip install -e '.[test]' - run: name: run tests command: | @@ -172,28 +78,12 @@ jobs: environment: MAX_HEAP_SIZE: 2048m HEAP_NEWSIZE: 512m - working_directory: ~/repo - steps: - checkout - - # Download and cache dependencies - - restore_cache: - keys: - - v1-dependencies-{{ checksum "requirements.txt" }} - # fallback to using the latest cache if no exact match is found - - v1-dependencies- - - run: name: install dependencies command: | - sudo apt-get update - sudo apt install lsb-release -y - curl -O https://packages.couchbase.com/releases/couchbase-release/couchbase-release-1.0-6-amd64.deb - sudo dpkg -i ./couchbase-release-1.0-6-amd64.deb - sudo apt-get update - sudo apt install libcouchbase-dev -y rm -rf venv export PATH=/home/circleci/.local/bin:$PATH pip install --user -U pip setuptools virtualenv @@ -201,13 +91,7 @@ jobs: . venv/bin/activate pip install -U pip python setup.py install_egg_info - pip install -r requirements-test.txt - - - save_cache: - paths: - - ./venv - key: v1-dependencies-{{ checksum "requirements.txt" }} - + pip install -e '.[test-cassandra]' - run: name: run tests command: | @@ -221,79 +105,37 @@ jobs: environment: MAX_HEAP_SIZE: 2048m HEAP_NEWSIZE: 512m - working_directory: ~/repo - steps: - checkout - - # Download and cache dependencies - - restore_cache: - keys: - - v1-dependencies-{{ checksum "requirements.txt" }} - # fallback to using the latest cache if no exact match is found - - v1-dependencies- - - run: name: install dependencies command: | - sudo apt-get update - sudo apt install lsb-release -y - curl -O https://packages.couchbase.com/releases/couchbase-release/couchbase-release-1.0-6-amd64.deb - sudo dpkg -i ./couchbase-release-1.0-6-amd64.deb - sudo apt-get update - sudo apt install libcouchbase-dev -y python -m venv venv . venv/bin/activate pip install -U pip python setup.py install_egg_info - pip install -r requirements.txt - pip install -r requirements-test.txt - - - save_cache: - paths: - - ./venv - key: v1-dependencies-{{ checksum "requirements.txt" }} - + pip install -e '.[test-cassandra]' - run: name: run tests command: | . venv/bin/activate nosetests -v tests/test_cassandra-driver.py:TestCassandra - gevent: + gevent38: docker: - image: circleci/python:3.8.2 - working_directory: ~/repo - steps: - checkout - - # Download and cache dependencies - - restore_cache: - keys: - - v1-dependencies-{{ checksum "requirements.txt" }} - # fallback to using the latest cache if no exact match is found - - v1-dependencies- - - run: name: install dependencies command: | - sudo apt-get update - sudo apt install lsb-release -y python -m venv venv . venv/bin/activate pip install -U pip python setup.py install_egg_info - pip install -r requirements.txt - pip install -r requirements-test.txt - - - save_cache: - paths: - - ./venv - key: v1-dependencies-{{ checksum "requirements.txt" }} - + pip install -e '.[test-gevent]' - run: name: run tests command: | @@ -307,4 +149,4 @@ workflows: - python38 - py27cassandra - py36cassandra - - gevent + - gevent38 diff --git a/runtests.py b/runtests.py index b0f9420a..2da7e5e8 100644 --- a/runtests.py +++ b/runtests.py @@ -1,12 +1,13 @@ +import os import sys import nose from distutils.version import LooseVersion command_line = [__file__, '--verbose'] -# Cassandra tests are run in dedicated jobs on CircleCI and will +# Cassandra and gevent tests are run in dedicated jobs on CircleCI and will # be run explicitly. (So always exclude them here) -command_line.extend(['-e', 'cassandra']) +command_line.extend(['-e', 'cassandra', '-e', 'gevent']) if LooseVersion(sys.version) < LooseVersion('3.5.3'): command_line.extend(['-e', 'asynqp', '-e', 'aiohttp', diff --git a/setup.py b/setup.py index 43f46f2a..fb615088 100644 --- a/setup.py +++ b/setup.py @@ -66,16 +66,26 @@ def check_setuptools(): 'django19': ['string = instana:load'], # deprecated: use same as 'instana' }, extras_require={ + 'test-gevent': [ + 'gevent>=1.4.0' + 'mock>=2.0.0', + 'nose>=1.0', + 'urllib3[secure]>=1.15' + ], + 'test-cassandra': [ + 'cassandra-driver==3.20.2', + 'mock>=2.0.0', + 'nose>=1.0', + 'urllib3[secure]>=1.15' + ], 'test': [ 'aiohttp>=3.5.4;python_version>="3.5"', 'asynqp>=0.4;python_version>="3.5"', 'couchbase==2.5.9', - 'cassandra-driver==3.20.2', 'django>=1.11,<2.2', 'nose>=1.0', 'flask>=0.12.2', 'grpcio>=1.18.0', - 'gevent>=1.4.0', 'lxml>=3.4', 'mock>=2.0.0', 'mysqlclient>=1.3.14;python_version>="3.5"', From 1bb51275145ce810eb2baab43e93a3f663fc425b Mon Sep 17 00:00:00 2001 From: Peter Giacomo Lombardo Date: Tue, 12 May 2020 16:33:12 +0200 Subject: [PATCH 09/18] Fix Cassandra tests dependencies --- .circleci/config.yml | 6 +++--- tests/__init__.py | 24 ++++++++++++------------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b0f3d813..4c6a683a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -96,7 +96,7 @@ jobs: name: run tests command: | . venv/bin/activate - nosetests -v tests/test_cassandra-driver.py:TestCassandra + CASSANDRA_TEST=1 nosetests -v tests/test_cassandra-driver.py:TestCassandra py36cassandra: docker: @@ -120,7 +120,7 @@ jobs: name: run tests command: | . venv/bin/activate - nosetests -v tests/test_cassandra-driver.py:TestCassandra + CASSANDRA_TEST=1 nosetests -v tests/test_cassandra-driver.py:TestCassandra gevent38: docker: @@ -140,7 +140,7 @@ jobs: name: run tests command: | . venv/bin/activate - GEVENT=1 nosetests -v tests/test_gevent + GEVENT_TEST=1 nosetests -v tests/test_gevent.py workflows: version: 2 build: diff --git a/tests/__init__.py b/tests/__init__.py index 751a1839..c900dd92 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -11,20 +11,20 @@ import time import threading -from .apps.flaskalino import flask_server +if 'CASSANDRA_TEST' not in os.environ: + from .apps.flaskalino import flask_server + # Background Flask application + # + # Spawn our background Flask app that the tests will throw + # requests at. + flask = threading.Thread(target=flask_server.serve_forever) + flask.daemon = True + flask.name = "Background Flask app" + print("Starting background Flask app...") + flask.start() -# Background Flask application -# -# Spawn our background Flask app that the tests will throw -# requests at. -flask = threading.Thread(target=flask_server.serve_forever) -flask.daemon = True -flask.name = "Background Flask app" -print("Starting background Flask app...") -flask.start() - -if 'GEVENT_TEST' not in os.environ: +if 'GEVENT_TEST' not in os.environ and 'CASSANDRA_TEST' not in os.environ: if sys.version_info >= (3, 5, 3): # Background RPC application From 2af2536f7cbf735421e3439a110a33e1bd582d41 Mon Sep 17 00:00:00 2001 From: Peter Giacomo Lombardo Date: Tue, 12 May 2020 16:44:44 +0200 Subject: [PATCH 10/18] Conditional gevent imports --- instana/instrumentation/gevent_inst.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/instana/instrumentation/gevent_inst.py b/instana/instrumentation/gevent_inst.py index 42eff059..37ea2048 100644 --- a/instana/instrumentation/gevent_inst.py +++ b/instana/instrumentation/gevent_inst.py @@ -2,8 +2,7 @@ import sys from ..log import logger -from opentracing.scope_managers.gevent import _GeventScope -from ..singletons import agent, tracer +from ..singletons import tracer try: if 'gevent' in sys.modules: @@ -11,6 +10,7 @@ import gevent from opentracing.scope_managers.gevent import GeventScopeManager + from opentracing.scope_managers.gevent import _GeventScope def spawn_callback(gr): parent_scope = tracer.scope_manager.active From 15cee87c776697a405dd45a183681813ceac9a53 Mon Sep 17 00:00:00 2001 From: Peter Giacomo Lombardo Date: Wed, 13 May 2020 11:51:40 +0200 Subject: [PATCH 11/18] gevent tests require flask --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index fb615088..145e4ec4 100644 --- a/setup.py +++ b/setup.py @@ -67,6 +67,7 @@ def check_setuptools(): }, extras_require={ 'test-gevent': [ + 'flask>=0.12.2', 'gevent>=1.4.0' 'mock>=2.0.0', 'nose>=1.0', From 8dacd6178432354df8d8bc1bcfc4371883a43db9 Mon Sep 17 00:00:00 2001 From: Peter Giacomo Lombardo Date: Wed, 13 May 2020 14:23:20 +0200 Subject: [PATCH 12/18] Use stretch image for 3.8 tests --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4c6a683a..6c43cd0b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -41,7 +41,7 @@ jobs: python38: docker: - - image: circleci/python:3.8.2 + - image: circleci/python:3.7.7-stretch - image: circleci/postgres:9.6.5-alpine-ram - image: circleci/mariadb:10-ram - image: circleci/redis:5.0.4 From 1f7c9fe1aafa20368735e2d9f3d764ebd31ee762 Mon Sep 17 00:00:00 2001 From: Peter Giacomo Lombardo Date: Wed, 13 May 2020 16:21:10 +0200 Subject: [PATCH 13/18] Add version check to gevent intrumentation --- instana/instrumentation/gevent_inst.py | 33 ++++++++++++++------------ 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/instana/instrumentation/gevent_inst.py b/instana/instrumentation/gevent_inst.py index 37ea2048..a5e81b89 100644 --- a/instana/instrumentation/gevent_inst.py +++ b/instana/instrumentation/gevent_inst.py @@ -6,23 +6,26 @@ try: if 'gevent' in sys.modules: - logger.debug("Instrumenting gevent") + if sys.modules['gevent'].version_info < (1,4): + logger.debug("gevent < 1.4 detected. The Instana package supports versions 1.4 and greater.") + else: + logger.debug("Instrumenting gevent") - import gevent - from opentracing.scope_managers.gevent import GeventScopeManager - from opentracing.scope_managers.gevent import _GeventScope + import gevent + from opentracing.scope_managers.gevent import GeventScopeManager + from opentracing.scope_managers.gevent import _GeventScope - def spawn_callback(gr): - parent_scope = tracer.scope_manager.active - if parent_scope is not None: - # New greenlet, new clean slate. Clone and make active in this new greenlet - # the currently active scope (but don't close this span on close - it's a - # clone/not the original) - parent_scope_clone = _GeventScope(parent_scope.manager, parent_scope.span, finish_on_close=False) - tracer._scope_manager._set_greenlet_scope(parent_scope_clone, gr) + def spawn_callback(gr): + parent_scope = tracer.scope_manager.active + if parent_scope is not None: + # New greenlet, new clean slate. Clone and make active in this new greenlet + # the currently active scope (but don't close this span on close - it's a + # clone/not the original) + parent_scope_clone = _GeventScope(parent_scope.manager, parent_scope.span, finish_on_close=False) + tracer._scope_manager._set_greenlet_scope(parent_scope_clone, gr) - logger.debug(" -> Updating tracer to use gevent based context management") - tracer._scope_manager = GeventScopeManager() - gevent.Greenlet.add_spawn_callback(spawn_callback) + logger.debug(" -> Updating tracer to use gevent based context management") + tracer._scope_manager = GeventScopeManager() + gevent.Greenlet.add_spawn_callback(spawn_callback) except ImportError: pass From 624825fe85d0cdcaf915c5f580ad44b0029d3cdb Mon Sep 17 00:00:00 2001 From: Peter Giacomo Lombardo Date: Wed, 13 May 2020 17:13:04 +0200 Subject: [PATCH 14/18] gevent based agent booting --- instana/__init__.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/instana/__init__.py b/instana/__init__.py index 569f483e..0a794d46 100644 --- a/instana/__init__.py +++ b/instana/__init__.py @@ -95,6 +95,16 @@ def lambda_handler(event, context): print("Couldn't determine and locate default function handler: %s.%s", module_name, function_name) +def boot_agent_later(): + """ Executes in the future! """ + if 'gevent' in sys.modules: + import gevent + gevent.spawn_later(2.0, boot_agent) + else: + t = Timer(2.0, boot_agent) + t.start() + + def boot_agent(): """Initialize the Instana agent and conditionally load auto-instrumentation.""" # Disable all the unused-import violations in this function @@ -174,7 +184,6 @@ def boot_agent(): else: if "INSTANA_MAGIC" in os.environ: # If we're being loaded into an already running process, then delay agent initialization - t = Timer(2.0, boot_agent) - t.start() + boot_agent_later() else: boot_agent() From fbbb2d34370a1d19d40e3e960ae84d9b7daa469c Mon Sep 17 00:00:00 2001 From: Peter Giacomo Lombardo Date: Wed, 13 May 2020 17:23:33 +0200 Subject: [PATCH 15/18] Linter improvements and refactoring --- instana/instrumentation/gevent_inst.py | 60 +++++++++++++++----------- instana/instrumentation/urllib3.py | 3 +- 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/instana/instrumentation/gevent_inst.py b/instana/instrumentation/gevent_inst.py index a5e81b89..b2d0c760 100644 --- a/instana/instrumentation/gevent_inst.py +++ b/instana/instrumentation/gevent_inst.py @@ -1,31 +1,41 @@ +""" +Instrumentation for the gevent package. +""" from __future__ import absolute_import import sys from ..log import logger from ..singletons import tracer -try: - if 'gevent' in sys.modules: - if sys.modules['gevent'].version_info < (1,4): - logger.debug("gevent < 1.4 detected. The Instana package supports versions 1.4 and greater.") - else: - logger.debug("Instrumenting gevent") - - import gevent - from opentracing.scope_managers.gevent import GeventScopeManager - from opentracing.scope_managers.gevent import _GeventScope - - def spawn_callback(gr): - parent_scope = tracer.scope_manager.active - if parent_scope is not None: - # New greenlet, new clean slate. Clone and make active in this new greenlet - # the currently active scope (but don't close this span on close - it's a - # clone/not the original) - parent_scope_clone = _GeventScope(parent_scope.manager, parent_scope.span, finish_on_close=False) - tracer._scope_manager._set_greenlet_scope(parent_scope_clone, gr) - - logger.debug(" -> Updating tracer to use gevent based context management") - tracer._scope_manager = GeventScopeManager() - gevent.Greenlet.add_spawn_callback(spawn_callback) -except ImportError: - pass + +def instrument_gevent(): + """ Adds context propagation to gevent greenlet spawning """ + try: + logger.debug("Instrumenting gevent") + + import gevent + from opentracing.scope_managers.gevent import GeventScopeManager + from opentracing.scope_managers.gevent import _GeventScope + + def spawn_callback(new_greenlet): + """ Handles context propagation for newly spawning greenlets """ + parent_scope = tracer.scope_manager.active + if parent_scope is not None: + # New greenlet, new clean slate. Clone and make active in this new greenlet + # the currently active scope (but don't close this span on close - it's a + # clone/not the original) + parent_scope_clone = _GeventScope(parent_scope.manager, parent_scope.span, finish_on_close=False) + tracer._scope_manager._set_greenlet_scope(parent_scope_clone, new_greenlet) + + logger.debug(" -> Updating tracer to use gevent based context management") + tracer._scope_manager = GeventScopeManager() + gevent.Greenlet.add_spawn_callback(spawn_callback) + except: + logger.debug("instrument_gevent: ", exc_info=True) + + +if 'gevent' in sys.modules: + if sys.modules['gevent'].version_info < (1, 4): + logger.debug("gevent < 1.4 detected. The Instana package supports versions 1.4 and greater.") + else: + instrument_gevent() diff --git a/instana/instrumentation/urllib3.py b/instana/instrumentation/urllib3.py index 51a78dd3..ab5f2889 100644 --- a/instana/instrumentation/urllib3.py +++ b/instana/instrumentation/urllib3.py @@ -13,8 +13,8 @@ def collect(instance, args, kwargs): """ Build and return a fully qualified URL for this request """ + kvs = dict() try: - kvs = dict() kvs['host'] = instance.host kvs['port'] = instance.port @@ -58,7 +58,6 @@ def collect_response(scope, response): except Exception: logger.debug("collect_response", exc_info=True) - @wrapt.patch_function_wrapper('urllib3', 'HTTPConnectionPool.urlopen') def urlopen_with_instana(wrapped, instance, args, kwargs): parent_span = tracer.active_span From 97fac4e43e49290a37ef8ff52b63073cc186f88a Mon Sep 17 00:00:00 2001 From: Peter Giacomo Lombardo Date: Wed, 13 May 2020 18:42:49 +0200 Subject: [PATCH 16/18] Update warning re: uWSGI threads. Now gevent support. --- instana/hooks/hook_uwsgi.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/instana/hooks/hook_uwsgi.py b/instana/hooks/hook_uwsgi.py index e5d82358..78885042 100644 --- a/instana/hooks/hook_uwsgi.py +++ b/instana/hooks/hook_uwsgi.py @@ -15,9 +15,9 @@ opt_master = uwsgi.opt.get('master', False) opt_lazy_apps = uwsgi.opt.get('lazy-apps', False) - if uwsgi.opt.get('enable-threads', False) is False: - logger.warn("Required: uWSGI threads are not enabled. " + - "Please enable by using the uWSGI --enable-threads option.") + if uwsgi.opt.get('enable-threads', False) is False and uwsgi.opt.get('gevent', False) is False: + logger.warn("Required: Neither uWSGI threads or gevent is enabled. " + + "Please enable by using the uWSGI --enable-threads or --gevent option.") if opt_master and opt_lazy_apps is False: # --master is supplied in uWSGI options (otherwise uwsgidecorators package won't be available) From fb68e82c71b2cafd7748446aa6f6fb32ae242e37 Mon Sep 17 00:00:00 2001 From: Peter Giacomo Lombardo Date: Fri, 15 May 2020 13:18:51 +0000 Subject: [PATCH 17/18] Add tests for imap unordered --- tests/test_gevent.py | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/tests/test_gevent.py b/tests/test_gevent.py index db8ca6b8..ce3ff3a9 100644 --- a/tests/test_gevent.py +++ b/tests/test_gevent.py @@ -1,6 +1,7 @@ from __future__ import absolute_import import gevent +from gevent.pool import Group import unittest import urllib3 @@ -21,7 +22,7 @@ def tearDown(self): """ Do nothing for now """ pass - def make_http_call(self): + def make_http_call(self, n=None): return self.http.request('GET', testenv["wsgi_server"] + '/') def spawn_calls(self): @@ -32,6 +33,13 @@ def spawn_calls(self): jobs.append(gevent.spawn(self.make_http_call)) gevent.joinall(jobs, timeout=2) + def spawn_imap_unordered(self): + igroup = Group() + result = [] + with tracer.start_active_span('test'): + for i in igroup.imap_unordered(self.make_http_call, range(3)): + result.append(i) + def launch_gevent_chain(self): with tracer.start_active_span('test'): gevent.spawn(self.spawn_calls).join() @@ -77,3 +85,35 @@ def test_spawning(self): self.assertIsNotNone(wsgi_spans) self.assertEqual(len(wsgi_spans), 1) + def test_imap_unordered(self): + gevent.spawn(self.spawn_imap_unordered()) + + gevent.sleep(2) + + spans = self.recorder.queued_spans() + self.assertEqual(7, len(spans)) + + span_filter = lambda span: span.n == "sdk" \ + and span.data['sdk']['name'] == 'test' and span.p == None + test_spans = get_spans_by_filter(spans, span_filter) + self.assertIsNotNone(test_spans) + self.assertEqual(len(test_spans), 1) + + test_span = test_spans[0] + self.assertTrue(type(test_spans[0]) is SDKSpan) + + span_filter = lambda span: span.n == "urllib3" + urllib3_spans = get_spans_by_filter(spans, span_filter) + self.assertEqual(len(urllib3_spans), 3) + + for urllib3_span in urllib3_spans: + # spans should all have the same test span parent + self.assertEqual(urllib3_span.t, test_span.t) + self.assertEqual(urllib3_span.p, test_span.s) + + # find the wsgi span generated from this urllib3 request + span_filter = lambda span: span.n == "wsgi" and span.p == urllib3_span.s + wsgi_spans = get_spans_by_filter(spans, span_filter) + self.assertIsNotNone(wsgi_spans) + self.assertEqual(len(wsgi_spans), 1) + From 56994fc53a0a4c4dfb724304112d113beb089a24 Mon Sep 17 00:00:00 2001 From: Peter Giacomo Lombardo Date: Fri, 15 May 2020 13:23:19 +0000 Subject: [PATCH 18/18] Code comments and updated debug log messages --- instana/instrumentation/gevent_inst.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/instana/instrumentation/gevent_inst.py b/instana/instrumentation/gevent_inst.py index b2d0c760..80845722 100644 --- a/instana/instrumentation/gevent_inst.py +++ b/instana/instrumentation/gevent_inst.py @@ -22,8 +22,9 @@ def spawn_callback(new_greenlet): parent_scope = tracer.scope_manager.active if parent_scope is not None: # New greenlet, new clean slate. Clone and make active in this new greenlet - # the currently active scope (but don't close this span on close - it's a - # clone/not the original) + # the currently active scope (but don't finish() the span on close - it's a + # clone/not the original and we don't want to close it prematurely) + # TODO: Change to our own ScopeManagers parent_scope_clone = _GeventScope(parent_scope.manager, parent_scope.span, finish_on_close=False) tracer._scope_manager._set_greenlet_scope(parent_scope_clone, new_greenlet) @@ -36,6 +37,8 @@ def spawn_callback(new_greenlet): if 'gevent' in sys.modules: if sys.modules['gevent'].version_info < (1, 4): - logger.debug("gevent < 1.4 detected. The Instana package supports versions 1.4 and greater.") + logger.debug("gevent < 1.4 detected. The Instana package supports gevent versions 1.4 and greater.") else: instrument_gevent() +else: + logger.debug("Instrumenting gevent: gevent not detected or loaded. Nothing done.")