Skip to content

Commit 4da1073

Browse files
author
Emanuele Palazzetti
authored
Merge pull request #433 from palazzem/requests-service
[requests] provide a service name for the request Span
2 parents 80748f4 + 10b04ee commit 4da1073

File tree

8 files changed

+322
-100
lines changed

8 files changed

+322
-100
lines changed
Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,33 @@
11
"""
2-
To trace all HTTP calls from the requests library, patch the library like so::
2+
The ``requests`` integration traces all HTTP calls to internal or external services.
3+
Auto instrumentation is available using the ``patch`` function that **must be called
4+
before** importing the ``requests`` library. The following is an example::
35
4-
# Patch the requests library.
5-
from ddtrace.contrib.requests import patch
6-
patch()
6+
from ddtrace import patch
7+
patch(requests=True)
78
89
import requests
9-
requests.get("http://www.datadog.com")
10+
requests.get("https://www.datadoghq.com")
1011
11-
If you would prefer finer grained control without monkeypatching the requests'
12-
code, use a TracedSession object as you would a requests.Session::
12+
If you would prefer finer grained control, use a ``TracedSession`` object as you would a
13+
``requests.Session``::
1314
1415
from ddtrace.contrib.requests import TracedSession
1516
1617
session = TracedSession()
17-
session.get("http://www.datadog.com")
18+
session.get("https://www.datadoghq.com")
1819
19-
To enable distributed tracing, for example if you call, from requests, a web service
20-
which is also instrumented and want to have traces including both client and server sides::
20+
The library can be configured globally and per instance, using the Configuration API::
2121
22-
from ddtrace.contrib.requests import TracedSession
22+
from ddtrace import config
2323
24-
session = TracedSession()
25-
session.distributed_tracing = True
26-
session.get("http://host.lan/webservice")
24+
# enable distributed tracing globally
25+
config.requests['distributed_tracing'] = True
26+
27+
# change the service name only for this session
28+
session = Session()
29+
cfg = config.get_from(session)
30+
cfg['service_name'] = 'auth-api'
2731
"""
2832
from ...utils.importlib import require_modules
2933

@@ -32,5 +36,11 @@
3236

3337
with require_modules(required_modules) as missing_modules:
3438
if not missing_modules:
35-
from .patch import TracedSession, patch, unpatch
36-
__all__ = ['TracedSession', 'patch', 'unpatch']
39+
from .patch import patch, unpatch
40+
from .session import TracedSession
41+
42+
__all__ = [
43+
'patch',
44+
'unpatch',
45+
'TracedSession',
46+
]
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import logging
2+
import ddtrace
3+
4+
from ddtrace import config
5+
6+
from .constants import DEFAULT_SERVICE
7+
8+
from ...ext import http
9+
from ...propagation.http import HTTPPropagator
10+
11+
12+
log = logging.getLogger(__name__)
13+
14+
15+
def _extract_service_name(session, span):
16+
"""Extracts the right service name based on the following logic:
17+
- `requests` is the default service name
18+
- users can change it via `session.service_name = 'clients'`
19+
- if the Span doesn't have a parent, use the set service name
20+
or fallback to the default
21+
- if the Span has a parent, use the set service name or the
22+
parent service value if the set service name is the default
23+
24+
The priority can be represented as:
25+
Updated service name > parent service name > default to `requests`.
26+
"""
27+
service_name = config.get_from(session)['service_name']
28+
if (service_name == DEFAULT_SERVICE and
29+
span._parent is not None and
30+
span._parent.service is not None):
31+
service_name = span._parent.service
32+
return service_name
33+
34+
35+
def _wrap_request(func, instance, args, kwargs):
36+
"""Trace the `Session.request` instance method"""
37+
# TODO[manu]: we already offer a way to provide the Global Tracer
38+
# and is ddtrace.tracer; it's used only inside our tests and can
39+
# be easily changed by providing a TracingTestCase that sets common
40+
# tracing functionalities.
41+
tracer = getattr(instance, 'datadog_tracer', ddtrace.tracer)
42+
43+
# skip if tracing is not enabled
44+
if not tracer.enabled:
45+
return func(*args, **kwargs)
46+
47+
method = kwargs.get('method') or args[0]
48+
url = kwargs.get('url') or args[1]
49+
headers = kwargs.get('headers', {})
50+
51+
with tracer.trace("requests.request", span_type=http.TYPE) as span:
52+
# update the span service name before doing any action
53+
span.service = _extract_service_name(instance, span)
54+
55+
# propagate distributed tracing headers
56+
if config.get_from(instance).get('distributed_tracing'):
57+
propagator = HTTPPropagator()
58+
propagator.inject(span.context, headers)
59+
kwargs['headers'] = headers
60+
61+
response = None
62+
try:
63+
response = func(*args, **kwargs)
64+
return response
65+
finally:
66+
try:
67+
span.set_tag(http.METHOD, method.upper())
68+
span.set_tag(http.URL, url)
69+
if response is not None:
70+
span.set_tag(http.STATUS_CODE, response.status_code)
71+
# `span.error` must be an integer
72+
span.error = int(500 <= response.status_code)
73+
except Exception:
74+
log.debug("requests: error adding tags", exc_info=True)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DEFAULT_SERVICE = 'requests'

ddtrace/contrib/requests/legacy.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# [Deprecation]: this module contains deprecated functions
2+
# that will be removed in newer versions of the Tracer.
3+
from ddtrace import config
4+
5+
from ...utils.deprecation import deprecation
6+
7+
8+
def _distributed_tracing(self):
9+
"""Deprecated: this method has been deprecated in favor of
10+
the configuration system. It will be removed in newer versions
11+
of the Tracer.
12+
"""
13+
deprecation(
14+
name='client.distributed_tracing',
15+
message='Use the configuration object instead `config.get_from(client)[\'distributed_tracing\'`',
16+
version='1.0.0',
17+
)
18+
return config.get_from(self)['distributed_tracing']
19+
20+
21+
def _distributed_tracing_setter(self, value):
22+
"""Deprecated: this method has been deprecated in favor of
23+
the configuration system. It will be removed in newer versions
24+
of the Tracer.
25+
"""
26+
deprecation(
27+
name='client.distributed_tracing',
28+
message='Use the configuration object instead `config.get_from(client)[\'distributed_tracing\'] = value`',
29+
version='1.0.0',
30+
)
31+
config.get_from(self)['distributed_tracing'] = value

ddtrace/contrib/requests/patch.py

Lines changed: 26 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
1-
import os
2-
import logging
3-
4-
import wrapt
5-
import ddtrace
61
import requests
72

8-
from ...ext import http
9-
from ...propagation.http import HTTPPropagator
10-
from ...utils.formats import asbool
3+
from wrapt import wrap_function_wrapper as _w
4+
5+
from ddtrace import config
6+
7+
from ...pin import Pin
8+
from ...utils.formats import asbool, get_env
119
from ...utils.wrappers import unwrap as _u
10+
from .legacy import _distributed_tracing, _distributed_tracing_setter
11+
from .constants import DEFAULT_SERVICE
12+
from .connection import _wrap_request
1213

1314

14-
log = logging.getLogger(__name__)
15+
# requests default settings
16+
config._add('requests',{
17+
'service_name': get_env('requests', 'service_name', DEFAULT_SERVICE),
18+
'distributed_tracing': asbool(get_env('requests', 'distributed_tracing', False)),
19+
})
1520

1621

1722
def patch():
@@ -20,8 +25,18 @@ def patch():
2025
return
2126
setattr(requests, '__datadog_patch', True)
2227

23-
wrapt.wrap_function_wrapper('requests', 'Session.__init__', _session_initializer)
24-
wrapt.wrap_function_wrapper('requests', 'Session.request', _traced_request_func)
28+
_w('requests', 'Session.request', _wrap_request)
29+
Pin(
30+
service=config.requests['service_name'],
31+
_config=config.requests,
32+
).onto(requests.Session)
33+
34+
# [Backward compatibility]: `session.distributed_tracing` should point and
35+
# update the `Pin` configuration instead. This block adds a property so that
36+
# old implementations work as expected
37+
fn = property(_distributed_tracing)
38+
fn = fn.setter(_distributed_tracing_setter)
39+
requests.Session.distributed_tracing = fn
2540

2641

2742
def unpatch():
@@ -30,72 +45,4 @@ def unpatch():
3045
return
3146
setattr(requests, '__datadog_patch', False)
3247

33-
_u(requests.Session, '__init__')
3448
_u(requests.Session, 'request')
35-
36-
37-
def _session_initializer(func, instance, args, kwargs):
38-
"""Define settings when requests client is initialized"""
39-
func(*args, **kwargs)
40-
41-
# set tracer settings
42-
distributed_tracing = asbool(os.environ.get('DATADOG_REQUESTS_DISTRIBUTED_TRACING')) or False
43-
setattr(instance, 'distributed_tracing', distributed_tracing)
44-
45-
46-
def _traced_request_func(func, instance, args, kwargs):
47-
""" traced_request is a tracing wrapper for requests' Session.request
48-
instance method.
49-
"""
50-
51-
# perhaps a global tracer isn't what we want, so permit individual requests
52-
# sessions to have their own (with the standard global fallback)
53-
tracer = getattr(instance, 'datadog_tracer', ddtrace.tracer)
54-
55-
# [TODO:christian] replace this with a unified way of handling options (eg, Pin)
56-
distributed_tracing = getattr(instance, 'distributed_tracing', None)
57-
58-
# bail on the tracing if not enabled.
59-
if not tracer.enabled:
60-
return func(*args, **kwargs)
61-
62-
method = kwargs.get('method') or args[0]
63-
url = kwargs.get('url') or args[1]
64-
headers = kwargs.get('headers', {})
65-
66-
with tracer.trace("requests.request", span_type=http.TYPE) as span:
67-
if distributed_tracing:
68-
propagator = HTTPPropagator()
69-
propagator.inject(span.context, headers)
70-
kwargs['headers'] = headers
71-
72-
resp = None
73-
try:
74-
resp = func(*args, **kwargs)
75-
return resp
76-
finally:
77-
try:
78-
_apply_tags(span, method, url, resp)
79-
except Exception:
80-
log.debug("error patching tags", exc_info=True)
81-
82-
83-
def _apply_tags(span, method, url, response):
84-
""" apply_tags will patch the given span with tags about the given request. """
85-
span.set_tag(http.METHOD, method)
86-
span.set_tag(http.URL, url)
87-
if response is not None:
88-
span.set_tag(http.STATUS_CODE, response.status_code)
89-
# `span.error` must be an integer
90-
span.error = int(500 <= response.status_code)
91-
92-
93-
class TracedSession(requests.Session):
94-
""" TracedSession is a requests' Session that is already patched.
95-
"""
96-
pass
97-
98-
99-
# Always patch our traced session with the traced method (cheesy way of sharing
100-
# code)
101-
wrapt.wrap_function_wrapper(TracedSession, 'request', _traced_request_func)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import requests
2+
3+
from wrapt import wrap_function_wrapper as _w
4+
5+
from .connection import _wrap_request
6+
7+
8+
class TracedSession(requests.Session):
9+
"""TracedSession is a requests' Session that is already traced.
10+
You can use it if you want a finer grained control for your
11+
HTTP clients.
12+
"""
13+
pass
14+
15+
16+
# always patch our `TracedSession` when imported
17+
_w(TracedSession, 'request', _wrap_request)

0 commit comments

Comments
 (0)