Skip to content

Commit 21d50d4

Browse files
authored
chore(tracing): move Pin and TraceFilter from ddtrace to ddtrace.trace [3.0] (#11899)
## Motivation In 3.0 all tracing specific objects should be defined in either `ddtrace.trace` or `ddtrace._trace` packages. This will allow us to better decouple tracing from other Datadog products. ## Changes - Exposes `Pin` and `TraceFilter` classes in `ddtrace.trace.__init__.py`. This keeps these objects in the public API. - Internalize the implementation details of the `Pin` and `TraceFilter` classes by moving `pin.py` and `filters.py` to the `ddtrace._trace` package. - Maintains backwards compatibility by continuing to export all objects in `ddtrace._trace.[pin/filters].py` in `ddtrace.[pin/filters].py`. - Logs a warning if the deprecated filters and pin modules are used. ## Checklist - [ ] PR author has checked that all the criteria below are met - The PR description includes an overview of the change - The PR description articulates the motivation for the change - The change includes tests OR the PR description describes a testing strategy - The PR description notes risks associated with the change, if any - Newly-added code is easy to change - The change follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) - The change includes or references documentation updates if necessary - Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [x] Reviewer has checked that all the criteria below are met - Title is accurate - All changes are related to the pull request's stated goal - Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes - Testing strategy adequately addresses listed risks - Newly-added code is easy to change - Release note makes sense to a user of the library - If necessary, author has acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment - Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)
1 parent 560aa89 commit 21d50d4

File tree

234 files changed

+605
-546
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

234 files changed

+605
-546
lines changed

Diff for: benchmarks/bm/utils.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88

99
from ddtrace import __version__ as ddtrace_version
1010
from ddtrace._trace.span import Span
11-
from ddtrace.filters import TraceFilter
1211
from ddtrace.internal import telemetry
12+
from ddtrace.trace import TraceFilter
1313

1414

1515
_Span = Span

Diff for: ddtrace/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from ._monkey import patch_all # noqa: E402
2727
from .internal.compat import PYTHON_VERSION_INFO # noqa: E402
2828
from .internal.utils.deprecations import DDTraceDeprecationWarning # noqa: E402
29-
from .pin import Pin # noqa: E402
29+
from ddtrace._trace.pin import Pin # noqa: E402
3030
from ddtrace._trace.span import Span # noqa: E402
3131
from ddtrace._trace.tracer import Tracer # noqa: E402
3232
from ddtrace.vendor import debtcollector
@@ -67,6 +67,7 @@
6767
_DEPRECATED_MODULE_ATTRIBUTES = [
6868
"Span",
6969
"Tracer",
70+
"Pin",
7071
]
7172

7273

Diff for: ddtrace/_trace/filters.py

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import abc
2+
import re
3+
from typing import TYPE_CHECKING # noqa:F401
4+
from typing import List # noqa:F401
5+
from typing import Optional # noqa:F401
6+
from typing import Union # noqa:F401
7+
8+
from ddtrace._trace.processor import TraceProcessor
9+
from ddtrace.ext import http
10+
11+
12+
if TYPE_CHECKING: # pragma: no cover
13+
from ddtrace._trace.span import Span # noqa:F401
14+
15+
16+
class TraceFilter(TraceProcessor):
17+
@abc.abstractmethod
18+
def process_trace(self, trace):
19+
# type: (List[Span]) -> Optional[List[Span]]
20+
"""Processes a trace.
21+
22+
None can be returned to prevent the trace from being exported.
23+
"""
24+
pass
25+
26+
27+
class FilterRequestsOnUrl(TraceFilter):
28+
r"""Filter out traces from incoming http requests based on the request's url.
29+
30+
This class takes as argument a list of regular expression patterns
31+
representing the urls to be excluded from tracing. A trace will be excluded
32+
if its root span contains a ``http.url`` tag and if this tag matches any of
33+
the provided regular expression using the standard python regexp match
34+
semantic (https://docs.python.org/3/library/re.html#re.match).
35+
36+
:param list regexps: a list of regular expressions (or a single string) defining
37+
the urls that should be filtered out.
38+
39+
Examples:
40+
To filter out http calls to domain api.example.com::
41+
42+
FilterRequestsOnUrl(r'http://api\\.example\\.com')
43+
44+
To filter out http calls to all first level subdomains from example.com::
45+
46+
FilterRequestOnUrl(r'http://.*+\\.example\\.com')
47+
48+
To filter out calls to both http://test.example.com and http://example.com/healthcheck::
49+
50+
FilterRequestOnUrl([r'http://test\\.example\\.com', r'http://example\\.com/healthcheck'])
51+
"""
52+
53+
def __init__(self, regexps: Union[str, List[str]]):
54+
if isinstance(regexps, str):
55+
regexps = [regexps]
56+
self._regexps = [re.compile(regexp) for regexp in regexps]
57+
58+
def process_trace(self, trace):
59+
# type: (List[Span]) -> Optional[List[Span]]
60+
"""
61+
When the filter is registered in the tracer, process_trace is called by
62+
on each trace before it is sent to the agent, the returned value will
63+
be fed to the next filter in the list. If process_trace returns None,
64+
the whole trace is discarded.
65+
"""
66+
for span in trace:
67+
url = span.get_tag(http.URL)
68+
if span.parent_id is None and url is not None:
69+
for regexp in self._regexps:
70+
if regexp.match(url):
71+
return None
72+
return trace

Diff for: ddtrace/_trace/pin.py

+209
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
from typing import TYPE_CHECKING # noqa:F401
2+
from typing import Any # noqa:F401
3+
from typing import Dict # noqa:F401
4+
from typing import Optional # noqa:F401
5+
6+
import wrapt
7+
8+
import ddtrace
9+
10+
from ..internal.logger import get_logger
11+
12+
13+
log = get_logger(__name__)
14+
15+
16+
# To set attributes on wrapt proxy objects use this prefix:
17+
# http://wrapt.readthedocs.io/en/latest/wrappers.html
18+
_DD_PIN_NAME = "_datadog_pin"
19+
_DD_PIN_PROXY_NAME = "_self_" + _DD_PIN_NAME
20+
21+
22+
class Pin(object):
23+
"""Pin (a.k.a Patch INfo) is a small class which is used to
24+
set tracing metadata on a particular traced connection.
25+
This is useful if you wanted to, say, trace two different
26+
database clusters.
27+
28+
>>> conn = sqlite.connect('/tmp/user.db')
29+
>>> # Override a pin for a specific connection
30+
>>> pin = Pin.override(conn, service='user-db')
31+
>>> conn = sqlite.connect('/tmp/image.db')
32+
"""
33+
34+
__slots__ = ["tags", "tracer", "_target", "_config", "_initialized"]
35+
36+
def __init__(
37+
self,
38+
service=None, # type: Optional[str]
39+
tags=None, # type: Optional[Dict[str, str]]
40+
tracer=None,
41+
_config=None, # type: Optional[Dict[str, Any]]
42+
):
43+
# type: (...) -> None
44+
tracer = tracer or ddtrace.tracer
45+
self.tags = tags
46+
self.tracer = tracer
47+
self._target = None # type: Optional[int]
48+
# keep the configuration attribute internal because the
49+
# public API to access it is not the Pin class
50+
self._config = _config or {} # type: Dict[str, Any]
51+
# [Backward compatibility]: service argument updates the `Pin` config
52+
self._config["service_name"] = service
53+
self._initialized = True
54+
55+
@property
56+
def service(self):
57+
# type: () -> str
58+
"""Backward compatibility: accessing to `pin.service` returns the underlying
59+
configuration value.
60+
"""
61+
return self._config["service_name"]
62+
63+
def __setattr__(self, name, value):
64+
if getattr(self, "_initialized", False) and name != "_target":
65+
raise AttributeError("can't mutate a pin, use override() or clone() instead")
66+
super(Pin, self).__setattr__(name, value)
67+
68+
def __repr__(self):
69+
return "Pin(service=%s, tags=%s, tracer=%s)" % (self.service, self.tags, self.tracer)
70+
71+
@staticmethod
72+
def _find(*objs):
73+
# type: (Any) -> Optional[Pin]
74+
"""
75+
Return the first :class:`ddtrace.trace.Pin` found on any of the provided objects or `None` if none were found
76+
77+
78+
>>> pin = Pin._find(wrapper, instance, conn)
79+
80+
:param objs: The objects to search for a :class:`ddtrace.trace.Pin` on
81+
:type objs: List of objects
82+
:rtype: :class:`ddtrace.trace.Pin`, None
83+
:returns: The first found :class:`ddtrace.trace.Pin` or `None` is none was found
84+
"""
85+
for obj in objs:
86+
pin = Pin.get_from(obj)
87+
if pin:
88+
return pin
89+
return None
90+
91+
@staticmethod
92+
def get_from(obj):
93+
# type: (Any) -> Optional[Pin]
94+
"""Return the pin associated with the given object. If a pin is attached to
95+
`obj` but the instance is not the owner of the pin, a new pin is cloned and
96+
attached. This ensures that a pin inherited from a class is a copy for the new
97+
instance, avoiding that a specific instance overrides other pins values.
98+
99+
>>> pin = Pin.get_from(conn)
100+
101+
:param obj: The object to look for a :class:`ddtrace.trace.Pin` on
102+
:type obj: object
103+
:rtype: :class:`ddtrace.trace.Pin`, None
104+
:returns: :class:`ddtrace.trace.Pin` associated with the object or None
105+
"""
106+
if hasattr(obj, "__getddpin__"):
107+
return obj.__getddpin__()
108+
109+
pin_name = _DD_PIN_PROXY_NAME if isinstance(obj, wrapt.ObjectProxy) else _DD_PIN_NAME
110+
pin = getattr(obj, pin_name, None)
111+
# detect if the PIN has been inherited from a class
112+
if pin is not None and pin._target != id(obj):
113+
pin = pin.clone()
114+
pin.onto(obj)
115+
return pin
116+
117+
@classmethod
118+
def override(
119+
cls,
120+
obj, # type: Any
121+
service=None, # type: Optional[str]
122+
tags=None, # type: Optional[Dict[str, str]]
123+
tracer=None,
124+
):
125+
# type: (...) -> None
126+
"""Override an object with the given attributes.
127+
128+
That's the recommended way to customize an already instrumented client, without
129+
losing existing attributes.
130+
131+
>>> conn = sqlite.connect('/tmp/user.db')
132+
>>> # Override a pin for a specific connection
133+
>>> Pin.override(conn, service='user-db')
134+
"""
135+
if not obj:
136+
return
137+
138+
pin = cls.get_from(obj)
139+
if pin is None:
140+
Pin(service=service, tags=tags, tracer=tracer).onto(obj)
141+
else:
142+
pin.clone(service=service, tags=tags, tracer=tracer).onto(obj)
143+
144+
def enabled(self):
145+
# type: () -> bool
146+
"""Return true if this pin's tracer is enabled."""
147+
# inline to avoid circular imports
148+
from ddtrace.settings.asm import config as asm_config
149+
150+
return bool(self.tracer) and (self.tracer.enabled or asm_config._apm_opt_out)
151+
152+
def onto(self, obj, send=True):
153+
# type: (Any, bool) -> None
154+
"""Patch this pin onto the given object. If send is true, it will also
155+
queue the metadata to be sent to the server.
156+
"""
157+
# Actually patch it on the object.
158+
try:
159+
if hasattr(obj, "__setddpin__"):
160+
return obj.__setddpin__(self)
161+
162+
pin_name = _DD_PIN_PROXY_NAME if isinstance(obj, wrapt.ObjectProxy) else _DD_PIN_NAME
163+
164+
# set the target reference; any get_from, clones and retarget the new PIN
165+
self._target = id(obj)
166+
if self.service:
167+
ddtrace.config._add_extra_service(self.service)
168+
return setattr(obj, pin_name, self)
169+
except AttributeError:
170+
log.debug("can't pin onto object. skipping", exc_info=True)
171+
172+
def remove_from(self, obj):
173+
# type: (Any) -> None
174+
# Remove pin from the object.
175+
try:
176+
pin_name = _DD_PIN_PROXY_NAME if isinstance(obj, wrapt.ObjectProxy) else _DD_PIN_NAME
177+
178+
pin = Pin.get_from(obj)
179+
if pin is not None:
180+
delattr(obj, pin_name)
181+
except AttributeError:
182+
log.debug("can't remove pin from object. skipping", exc_info=True)
183+
184+
def clone(
185+
self,
186+
service=None, # type: Optional[str]
187+
tags=None, # type: Optional[Dict[str, str]]
188+
tracer=None,
189+
):
190+
# type: (...) -> Pin
191+
"""Return a clone of the pin with the given attributes replaced."""
192+
# do a shallow copy of Pin dicts
193+
if not tags and self.tags:
194+
tags = self.tags.copy()
195+
196+
# we use a copy instead of a deepcopy because we expect configurations
197+
# to have only a root level dictionary without nested objects. Using
198+
# deepcopy introduces a big overhead:
199+
#
200+
# copy: 0.00654911994934082
201+
# deepcopy: 0.2787208557128906
202+
config = self._config.copy()
203+
204+
return Pin(
205+
service=service or self.service,
206+
tags=tags,
207+
tracer=tracer or self.tracer, # do not clone the Tracer
208+
_config=config,
209+
)

Diff for: ddtrace/_trace/tracer.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
from ddtrace.constants import HOSTNAME_KEY
3535
from ddtrace.constants import PID
3636
from ddtrace.constants import VERSION_KEY
37-
from ddtrace.filters import TraceFilter
3837
from ddtrace.internal import agent
3938
from ddtrace.internal import atexit
4039
from ddtrace.internal import compat
@@ -69,6 +68,7 @@
6968
from ddtrace.settings import Config
7069
from ddtrace.settings.asm import config as asm_config
7170
from ddtrace.settings.peer_service import _ps_config
71+
from ddtrace.trace import TraceFilter
7272
from ddtrace.vendor.debtcollector import deprecate
7373

7474

Diff for: ddtrace/contrib/aiomysql/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
To configure the integration on an per-connection basis use the
2020
``Pin`` API::
2121
22-
from ddtrace import Pin
22+
from ddtrace.trace import Pin
2323
import asyncio
2424
import aiomysql
2525

Diff for: ddtrace/contrib/aiopg/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"""
22
Instrument aiopg to report a span for each executed Postgres queries::
33
4-
from ddtrace import Pin, patch
4+
from ddtrace import patch
5+
from ddtrace.trace import Pin
56
import aiopg
67
78
# If not patched yet, you can patch aiopg specifically

Diff for: ddtrace/contrib/aioredis/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
``Pin`` API::
6565
6666
import aioredis
67-
from ddtrace import Pin
67+
from ddtrace.trace import Pin
6868
6969
myaioredis = aioredis.Aioredis()
7070
Pin.override(myaioredis, service="myaioredis")

Diff for: ddtrace/contrib/anthropic/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@
7676
``Pin`` API::
7777
7878
import anthropic
79-
from ddtrace import Pin, config
79+
from ddtrace import config
80+
from ddtrace.trace import Pin
8081
8182
Pin.override(anthropic, service="my-anthropic-service")
8283
""" # noqa: E501

Diff for: ddtrace/contrib/aredis/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
To configure particular aredis instances use the :class:`Pin <ddtrace.Pin>` API::
5454
5555
import aredis
56-
from ddtrace import Pin
56+
from ddtrace.trace import Pin
5757
5858
client = aredis.StrictRedis(host="localhost", port=6379)
5959

Diff for: ddtrace/contrib/asyncpg/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
basis use the ``Pin`` API::
3939
4040
import asyncpg
41-
from ddtrace import Pin
41+
from ddtrace.trace import Pin
4242
4343
conn = asyncpg.connect("postgres://localhost:5432")
4444
Pin.override(conn, service="custom-service")

Diff for: ddtrace/contrib/cassandra/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
``import ddtrace.auto`` will automatically patch your Cluster instance to make it work.
44
::
55
6-
from ddtrace import Pin, patch
6+
from ddtrace import patch
7+
from ddtrace.trace import Pin
78
from cassandra.cluster import Cluster
89
910
# If not patched yet, you can patch cassandra specifically

0 commit comments

Comments
 (0)