Skip to content

Commit 8634e48

Browse files
authored
Merge pull request #74 from DataDog/talwai/gevent
[trace] add gevent buffer and docs
2 parents dcab74d + f080d82 commit 8634e48

File tree

7 files changed

+234
-2
lines changed

7 files changed

+234
-2
lines changed

ddtrace/buffer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ def get(self):
1212
raise NotImplementedError()
1313

1414

15-
class ThreadLocalSpanBuffer(object):
16-
""" ThreadLocalBuffer stores the current active span in thread-local
15+
class ThreadLocalSpanBuffer(SpanBuffer):
16+
""" ThreadLocalSpanBuffer stores the current active span in thread-local
1717
storage.
1818
"""
1919

ddtrace/contrib/gevent/__init__.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""
2+
To trace a request in a gevent-ed environment, configure the tracer to use greenlet-local
3+
storage, rather than the default thread-local storage.
4+
5+
This allows the tracer to pick up a transaction exactly
6+
where it left off as greenlets yield context to one another.
7+
8+
The simplest way to trace with greenlet-local storage is via the `gevent.monkey` module::
9+
10+
# Always monkey patch before importing the global tracer
11+
# Broadly, gevent recommends that patches happen as early as possible in the app lifecycle
12+
# http://www.gevent.org/gevent.monkey.html#patching
13+
14+
from gevent import monkey; monkey.patch_thread()
15+
# Alternatively, use monkey.patch_all() to perform all available patches
16+
17+
from ddtrace import tracer
18+
19+
import gevent
20+
21+
def my_parent_function():
22+
with tracer.trace("web.request") as span:
23+
span.service = "web"
24+
gevent.spawn(worker_function, span)
25+
26+
def worker_function(parent):
27+
# Set the active span
28+
tracer.span_buffer.set(parent)
29+
30+
# then trace its child
31+
with tracer.trace("greenlet.call") as span:
32+
span.service = "greenlet"
33+
...
34+
35+
with tracer.trace("greenlet.child_call") as child:
36+
...
37+
38+
Note that when spawning greenlets,
39+
the span object must be explicitly passed from the parent to coroutine context.
40+
A tracer in a freshly-spawned greenlet will not know about its parent span.
41+
42+
If you are unable to patch `gevent` in the global scope, you can configure
43+
the global tracer to use greenlet-local storage on an as-needed basis::
44+
45+
from ddtrace import tracer
46+
from ddtrace.contrib.gevent import GreenletLocalSpanBuffer
47+
48+
import gevent
49+
50+
def my_parent_function():
51+
with tracer.trace("web.request") as span:
52+
span.service = "web"
53+
gevent.spawn(worker_function, span)
54+
55+
def worker_function(parent):
56+
tracer.span_buffer = GreenletLocalSpanBuffer()
57+
# Set the active span
58+
tracer.span_buffer.set(parent)
59+
60+
# then trace its child
61+
with tracer.trace("greenlet.call") as span:
62+
span.service = "greenlet"
63+
...
64+
65+
with tracer.trace("greenlet.child_call") as child:
66+
...
67+
"""
68+
69+
from ..util import require_modules
70+
71+
required_modules = ['gevent', 'gevent.local']
72+
73+
with require_modules(required_modules) as missing_modules:
74+
if not missing_modules:
75+
from .buffer import GreenletLocalSpanBuffer
76+
__all__ = ['GreenletLocalSpanBuffer']

ddtrace/contrib/gevent/buffer.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import gevent.local
2+
from ddtrace.buffer import SpanBuffer
3+
4+
class GreenletLocalSpanBuffer(SpanBuffer):
5+
""" GreenletLocalSpanBuffer stores the current active span in greenlet-local
6+
storage.
7+
"""
8+
9+
def __init__(self):
10+
self._locals = gevent.local.local()
11+
12+
def set(self, span):
13+
self._locals.span = span
14+
15+
def get(self):
16+
return getattr(self._locals, 'span', None)
17+
18+
def pop(self):
19+
span = self.get()
20+
self.set(None)
21+
return span

ddtrace/contrib/sqlalchemy/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@
1010
trace_engine(engine, tracer, "my-database")
1111
1212
engine.connect().execute("select count(*) from users")
13+
14+
If you are using sqlalchemy in a gevent-ed environment, make sure to monkey patch
15+
the `thread` module prior to importing the global tracer::
16+
17+
from gevent import monkey; monkey.patch_thread() # or patch_all() if you prefer
18+
from ddtrace import tracer
19+
20+
# Add instrumentation to your engine as above
21+
...
1322
"""
1423

1524

docs/index.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,12 @@ Flask-cache
148148

149149
.. automodule:: ddtrace.contrib.flask_cache
150150

151+
152+
Gevent
153+
~~~~~~
154+
155+
.. automodule:: ddtrace.contrib.gevent
156+
151157
MongoDB
152158
~~~~~~~
153159

tests/contrib/gevent/test.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import unittest
2+
3+
from nose.tools import eq_, ok_
4+
from nose.plugins.attrib import attr
5+
import gevent
6+
import gevent.local
7+
import thread
8+
import threading
9+
10+
11+
class GeventGlobalScopeTest(unittest.TestCase):
12+
def setUp(self):
13+
# simulate standard app bootstrap
14+
from gevent import monkey; monkey.patch_thread()
15+
from ddtrace import tracer
16+
17+
def test_global_patch(self):
18+
from ddtrace import tracer; tracer.enabled = False
19+
20+
# Ensure the patch is active
21+
ok_(isinstance(tracer.span_buffer._locals, gevent.local.local))
22+
23+
seen_resources = []
24+
def worker_function(parent):
25+
tracer.span_buffer.set(parent)
26+
seen_resources.append(tracer.span_buffer.get().resource)
27+
28+
with tracer.trace("greenlet.call") as span:
29+
span.resource = "sibling"
30+
31+
gevent.sleep()
32+
33+
# Ensure we have the correct parent span even after a context switch
34+
eq_(tracer.span_buffer.get().span_id, span.span_id)
35+
with tracer.trace("greenlet.other_call") as child:
36+
child.resource = "sibling_child"
37+
38+
with tracer.trace("web.request") as span:
39+
span.service = "web"
40+
span.resource = "parent"
41+
worker_count = 5
42+
workers = [gevent.spawn(worker_function, span) for w in range(worker_count)]
43+
gevent.joinall(workers)
44+
45+
# Ensure all greenlets see the right parent span
46+
ok_("sibling" not in seen_resources)
47+
ok_(all(s == "parent" for s in seen_resources))
48+
49+
def tearDown(self):
50+
# undo gevent monkey patching
51+
reload(thread); reload(threading)
52+
from ddtrace.buffer import ThreadLocalSpanBuffer
53+
from ddtrace import tracer; tracer.span_buffer = ThreadLocalSpanBuffer()
54+
55+
56+
class GeventLocalScopeTest(unittest.TestCase):
57+
58+
def test_unpatched(self):
59+
"""
60+
Demonstrate a situation where thread-local storage leads to a bad tree:
61+
1. Main thread spawns several coroutines
62+
2. A coroutine is handed context from a sibling coroutine
63+
3. A coroutine incorrectly sees a "sibling" span as its parent
64+
"""
65+
from ddtrace import tracer; tracer.enabled = False
66+
67+
seen_resources = []
68+
def my_worker_function(i):
69+
ok_(tracer.span_buffer.get())
70+
seen_resources.append(tracer.span_buffer.get().resource)
71+
72+
with tracer.trace("greenlet.call") as span:
73+
span.resource = "sibling"
74+
gevent.sleep()
75+
76+
with tracer.trace("web.request") as span:
77+
span.service = "web"
78+
span.resource = "parent"
79+
80+
worker_count = 5
81+
workers = [gevent.spawn(my_worker_function, w) for w in range(worker_count)]
82+
gevent.joinall(workers)
83+
84+
# check that a bad parent span was seen
85+
ok_("sibling" in seen_resources)
86+
87+
def test_local_patch(self):
88+
"""
89+
Test patching a parent span into a coroutine's tracer
90+
"""
91+
from ddtrace import tracer; tracer.enabled = False
92+
from ddtrace.contrib.gevent import GreenletLocalSpanBuffer
93+
94+
def fn(parent):
95+
tracer.span_buffer = GreenletLocalSpanBuffer()
96+
tracer.span_buffer.set(parent)
97+
98+
with tracer.trace("greenlet.call") as span:
99+
span.service = "greenlet"
100+
101+
gevent.sleep()
102+
103+
# Ensure we have the correct parent span even after a context switch
104+
eq_(tracer.span_buffer.get().span_id, span.span_id)
105+
with tracer.trace("greenlet.child_call") as child:
106+
eq_(child.parent_id, span.span_id)
107+
108+
with tracer.trace("web.request") as span:
109+
span.service = "web"
110+
worker = gevent.spawn(fn, span)
111+
worker.join()
112+
113+
def tearDown(self):
114+
# undo gevent monkey patching
115+
reload(thread); reload(threading)
116+
from ddtrace.buffer import ThreadLocalSpanBuffer
117+
from ddtrace import tracer; tracer.span_buffer = ThreadLocalSpanBuffer()

tox.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ envlist =
1414
{py27,py34}-django{18,19,110}-djangopylibmc06-djangoredis45-pylibmc-redis-memcached
1515
{py27,py34}-flask{010,011}-blinker
1616
{py27,py34}-flask{010,011}-flaskcache{013}-memcached-redis-blinker
17+
{py27,py34}-gevent{10,11}
1718
{py27}-flask{010,011}-flaskcache{012}-memcached-redis-blinker
1819
{py27,py34}-mysqlconnector{21}
1920
{py27,py34}-pylibmc{140,150}
@@ -57,6 +58,8 @@ deps =
5758
djangoredis45: django-redis>=4.5,<4.6
5859
flask010: flask>=0.10,<0.11
5960
flask011: flask>=0.11
61+
gevent10: gevent>=1.0,<1.1
62+
gevent11: gevent>=1.1
6063
flaskcache012: flask_cache>=0.12,<0.13
6164
flaskcache013: flask_cache>=0.13,<0.14
6265
memcached: python-memcached

0 commit comments

Comments
 (0)