Skip to content

Commit 5a65259

Browse files
author
Emanuele Palazzetti
committed
Merge branch '0.13-dev' into 'master'
2 parents 8f203d0 + ec8d8d5 commit 5a65259

File tree

19 files changed

+928
-12
lines changed

19 files changed

+928
-12
lines changed

.circleci/config.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,27 @@ jobs:
521521
paths:
522522
- .tox
523523

524+
pymemcache:
525+
docker:
526+
- image: datadog/docker-library:dd_trace_py_1_0_0
527+
- image: memcached:1.4
528+
steps:
529+
- checkout
530+
- restore_cache:
531+
keys:
532+
- tox-cache-pymemcache-{{ checksum "tox.ini" }}
533+
- run: tox -e '{py27,py34,py35,py36}-pymemcache{130,140}' --result-json /tmp/pymemcache.1.results
534+
- run: tox -e '{py27,py34,py35,py36}-pymemcache-autopatch{130,140}' --result-json /tmp/pymemcache.2.results
535+
- persist_to_workspace:
536+
root: /tmp
537+
paths:
538+
- pymemcache.1.results
539+
- pymemcache.2.results
540+
- save_cache:
541+
key: tox-cache-pymemcache-{{ checksum "tox.ini" }}
542+
paths:
543+
- .tox
544+
524545
pymongo:
525546
docker:
526547
- image: datadog/docker-library:dd_trace_py_1_0_0
@@ -806,6 +827,7 @@ workflows:
806827
- mysqlconnector
807828
- mysqlpython
808829
- mysqldb
830+
- pymemcache
809831
- pymysql
810832
- pylibmc
811833
- pymongo
@@ -844,6 +866,7 @@ workflows:
844866
- mysqldb
845867
- pymysql
846868
- pylibmc
869+
- pymemcache
847870
- pymongo
848871
- pyramid
849872
- requests

ddtrace/api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
# project
88
from .encoding import get_encoder, JSONEncoder
9-
from .compat import httplib, PYTHON_VERSION, PYTHON_INTERPRETER
9+
from .compat import httplib, PYTHON_VERSION, PYTHON_INTERPRETER, get_connection_response
1010

1111

1212
log = logging.getLogger(__name__)
@@ -140,4 +140,4 @@ def _put(self, endpoint, data, count=0):
140140
headers[TRACE_COUNT_HEADER] = str(count)
141141

142142
conn.request("PUT", endpoint, data, headers)
143-
return conn.getresponse()
143+
return get_connection_response(conn)

ddtrace/compat.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,24 @@ def to_unicode(s):
7070
return stringify(s)
7171

7272

73+
def get_connection_response(conn):
74+
"""Returns the response for a connection.
75+
76+
If using Python 2 enable buffering.
77+
78+
Python 2 does not enable buffering by default resulting in many recv
79+
syscalls.
80+
81+
See:
82+
https://bugs.python.org/issue4879
83+
https://github.com/python/cpython/commit/3c43fcba8b67ea0cec4a443c755ce5f25990a6cf
84+
"""
85+
if PY2:
86+
return conn.getresponse(buffering=True)
87+
else:
88+
return conn.getresponse()
89+
90+
7391
if PY2:
7492
string_type = basestring
7593
msgpack_type = basestring
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""Instrument pymemcache to report memcached queries.
2+
3+
``patch_all`` will automatically patch the pymemcache ``Client``::
4+
5+
from ddtrace import Pin, patch
6+
7+
# If not patched yet, patch pymemcache specifically
8+
patch(pymemcache=True)
9+
10+
# Import reference to Client AFTER patching
11+
import pymemcache
12+
from pymemcache.client.base import Client
13+
14+
# Use a pin to specify metadata related all clients
15+
Pin.override(pymemcache, service='my-memcached-service')
16+
17+
# This will report a span with the default settings
18+
client = Client(('localhost', 11211))
19+
client.set("my-key", "my-val")
20+
21+
# Use a pin to specify metadata related to this particular client
22+
Pin.override(client, service='my-memcached-service')
23+
24+
Pymemcache's ``HashClient`` will also be indirectly patched as it uses
25+
``Client``s under the hood.
26+
"""
27+
28+
from .patch import patch, unpatch
29+
30+
__all__ = [patch, unpatch]
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
# stdlib
2+
import logging
3+
import sys
4+
5+
# 3p
6+
import wrapt
7+
import pymemcache
8+
from pymemcache.client.base import Client
9+
from pymemcache.exceptions import (
10+
MemcacheClientError,
11+
MemcacheServerError,
12+
MemcacheUnknownCommandError,
13+
MemcacheUnknownError,
14+
MemcacheIllegalInputError,
15+
)
16+
17+
# project
18+
from ddtrace import Pin
19+
from ddtrace.compat import reraise
20+
from ddtrace.ext import net, memcached as memcachedx
21+
22+
log = logging.getLogger(__name__)
23+
24+
25+
# keep a reference to the original unpatched clients
26+
_Client = Client
27+
28+
29+
class WrappedClient(wrapt.ObjectProxy):
30+
"""Wrapper providing patched methods of a pymemcache Client.
31+
32+
Relevant connection information is obtained during initialization and
33+
attached to each span.
34+
35+
Keys are tagged in spans for methods that act upon a key.
36+
"""
37+
38+
def __init__(self, *args, **kwargs):
39+
c = _Client(*args, **kwargs)
40+
super(WrappedClient, self).__init__(c)
41+
42+
# tags to apply to each span generated by this client
43+
tags = _get_address_tags(*args, **kwargs)
44+
45+
parent_pin = Pin.get_from(pymemcache)
46+
47+
if parent_pin:
48+
pin = parent_pin.clone(tags=tags)
49+
else:
50+
pin = Pin(tags=tags)
51+
52+
# attach the pin onto this instance
53+
pin.onto(self)
54+
55+
def set(self, *args, **kwargs):
56+
return self._traced_cmd("set", *args, **kwargs)
57+
58+
def set_many(self, *args, **kwargs):
59+
return self._traced_cmd("set_many", *args, **kwargs)
60+
61+
def add(self, *args, **kwargs):
62+
return self._traced_cmd("add", *args, **kwargs)
63+
64+
def replace(self, *args, **kwargs):
65+
return self._traced_cmd("replace", *args, **kwargs)
66+
67+
def append(self, *args, **kwargs):
68+
return self._traced_cmd("append", *args, **kwargs)
69+
70+
def prepend(self, *args, **kwargs):
71+
return self._traced_cmd("prepend", *args, **kwargs)
72+
73+
def cas(self, *args, **kwargs):
74+
return self._traced_cmd("cas", *args, **kwargs)
75+
76+
def get(self, *args, **kwargs):
77+
return self._traced_cmd("get", *args, **kwargs)
78+
79+
def get_many(self, *args, **kwargs):
80+
return self._traced_cmd("get_many", *args, **kwargs)
81+
82+
def gets(self, *args, **kwargs):
83+
return self._traced_cmd("gets", *args, **kwargs)
84+
85+
def gets_many(self, *args, **kwargs):
86+
return self._traced_cmd("gets_many", *args, **kwargs)
87+
88+
def delete(self, *args, **kwargs):
89+
return self._traced_cmd("delete", *args, **kwargs)
90+
91+
def delete_many(self, *args, **kwargs):
92+
return self._traced_cmd("delete_many", *args, **kwargs)
93+
94+
def incr(self, *args, **kwargs):
95+
return self._traced_cmd("incr", *args, **kwargs)
96+
97+
def decr(self, *args, **kwargs):
98+
return self._traced_cmd("decr", *args, **kwargs)
99+
100+
def touch(self, *args, **kwargs):
101+
return self._traced_cmd("touch", *args, **kwargs)
102+
103+
def stats(self, *args, **kwargs):
104+
return self._traced_cmd("stats", *args, **kwargs)
105+
106+
def version(self, *args, **kwargs):
107+
return self._traced_cmd("version", *args, **kwargs)
108+
109+
def flush_all(self, *args, **kwargs):
110+
return self._traced_cmd("flush_all", *args, **kwargs)
111+
112+
def quit(self, *args, **kwargs):
113+
return self._traced_cmd("quit", *args, **kwargs)
114+
115+
def set_multi(self, *args, **kwargs):
116+
"""set_multi is an alias for set_many"""
117+
return self._traced_cmd("set_many", *args, **kwargs)
118+
119+
def get_multi(self, *args, **kwargs):
120+
"""set_multi is an alias for set_many"""
121+
return self._traced_cmd("get_many", *args, **kwargs)
122+
123+
def _traced_cmd(self, method_name, *args, **kwargs):
124+
"""Run and trace the given command.
125+
126+
Any pymemcache exception is caught and span error information is
127+
set. The exception is then reraised for the application to handle
128+
appropriately.
129+
130+
Relevant tags are set in the span.
131+
"""
132+
method = getattr(self.__wrapped__, method_name)
133+
p = Pin.get_from(self)
134+
135+
# if the pin does not exist or is not enabled, shortcut
136+
if not p or not p.enabled():
137+
return method(*args, **kwargs)
138+
139+
with p.tracer.trace(
140+
memcachedx.CMD,
141+
service=p.service,
142+
resource=method_name,
143+
span_type=memcachedx.TYPE,
144+
) as span:
145+
# try to set relevant tags, catch any exceptions so we don't mess
146+
# with the application
147+
try:
148+
span.set_tags(p.tags)
149+
vals = _get_query_string(args)
150+
query = "{}{}{}".format(method_name, " " if vals else "", vals)
151+
span.set_tag(memcachedx.QUERY, query)
152+
except Exception:
153+
log.debug("Error setting relevant pymemcache tags")
154+
155+
try:
156+
return method(*args, **kwargs)
157+
except (
158+
MemcacheClientError,
159+
MemcacheServerError,
160+
MemcacheUnknownCommandError,
161+
MemcacheUnknownError,
162+
MemcacheIllegalInputError,
163+
):
164+
(typ, val, tb) = sys.exc_info()
165+
span.set_exc_info(typ, val, tb)
166+
reraise(typ, val, tb)
167+
168+
169+
def _get_address_tags(*args, **kwargs):
170+
"""Attempt to get host and port from args passed to Client initializer."""
171+
tags = {}
172+
try:
173+
if len(args):
174+
host, port = args[0]
175+
tags[net.TARGET_HOST] = host
176+
tags[net.TARGET_PORT] = port
177+
except Exception:
178+
log.debug("Error collecting client address tags")
179+
180+
return tags
181+
182+
183+
def _get_query_string(args):
184+
"""Return the query values given the arguments to a pymemcache command.
185+
186+
If there are multiple query values, they are joined together
187+
space-separated.
188+
"""
189+
keys = ""
190+
191+
# shortcut if no args
192+
if not args:
193+
return keys
194+
195+
# pull out the first arg which will contain any key
196+
arg = args[0]
197+
198+
# if we get a dict, convert to list of keys
199+
if type(arg) is dict:
200+
arg = list(arg)
201+
202+
if type(arg) is str:
203+
keys = arg
204+
elif type(arg) is bytes:
205+
keys = arg.decode()
206+
elif type(arg) is list and len(arg):
207+
if type(arg[0]) is str:
208+
keys = " ".join(arg)
209+
elif type(arg[0]) is bytes:
210+
keys = b" ".join(arg).decode()
211+
212+
return keys
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import pymemcache
2+
3+
from ddtrace.ext import memcached as memcachedx
4+
from ddtrace.pin import Pin, _DD_PIN_NAME, _DD_PIN_PROXY_NAME
5+
from .client import WrappedClient
6+
7+
_Client = pymemcache.client.base.Client
8+
9+
10+
def patch():
11+
if getattr(pymemcache.client, "_datadog_patch", False):
12+
return
13+
14+
setattr(pymemcache.client, "_datadog_patch", True)
15+
setattr(pymemcache.client.base, "Client", WrappedClient)
16+
17+
# Create a global pin with default configuration for our pymemcache clients
18+
Pin(
19+
app=memcachedx.SERVICE, service=memcachedx.SERVICE, app_type=memcachedx.TYPE
20+
).onto(pymemcache)
21+
22+
23+
def unpatch():
24+
"""Remove pymemcache tracing"""
25+
if not getattr(pymemcache.client, "_datadog_patch", False):
26+
return
27+
setattr(pymemcache.client, "_datadog_patch", False)
28+
setattr(pymemcache.client.base, "Client", _Client)
29+
30+
# Remove any pins that may exist on the pymemcache reference
31+
setattr(pymemcache, _DD_PIN_NAME, None)
32+
setattr(pymemcache, _DD_PIN_PROXY_NAME, None)

ddtrace/ext/memcached.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
2-
from ddtrace.ext import AppTypes
3-
1+
CMD = "memcached.command"
42
SERVICE = "memcached"
5-
TYPE = AppTypes.cache
6-
3+
TYPE = "memcached"
74
QUERY = "memcached.query"

ddtrace/monkey.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
'pymysql': True,
3030
'psycopg': True,
3131
'pylibmc': True,
32+
'pymemcache': True,
3233
'pymongo': True,
3334
'redis': True,
3435
'requests': False, # Not ready yet

ddtrace/tracer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import functools
22
import logging
3-
from os import getpid
3+
from os import environ, getpid
44

55
from .ext import system
66
from .provider import DefaultContextProvider
@@ -27,7 +27,7 @@ class Tracer(object):
2727
from ddtrace import tracer
2828
trace = tracer.trace("app.request", "web-server").finish()
2929
"""
30-
DEFAULT_HOSTNAME = 'localhost'
30+
DEFAULT_HOSTNAME = environ.get('DATADOG_TRACE_AGENT_HOSTNAME', 'localhost')
3131
DEFAULT_PORT = 8126
3232

3333
def __init__(self):

0 commit comments

Comments
 (0)