Skip to content

Commit

Permalink
Merge branch 'main' into brettlangdon/oci.dedupe
Browse files Browse the repository at this point in the history
  • Loading branch information
brettlangdon authored Jan 15, 2025
2 parents 8900d66 + 21d50d4 commit 5840592
Show file tree
Hide file tree
Showing 234 changed files with 605 additions and 546 deletions.
2 changes: 1 addition & 1 deletion benchmarks/bm/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@

from ddtrace import __version__ as ddtrace_version
from ddtrace._trace.span import Span
from ddtrace.filters import TraceFilter
from ddtrace.internal import telemetry
from ddtrace.trace import TraceFilter


_Span = Span
Expand Down
3 changes: 2 additions & 1 deletion ddtrace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from ._monkey import patch_all # noqa: E402
from .internal.compat import PYTHON_VERSION_INFO # noqa: E402
from .internal.utils.deprecations import DDTraceDeprecationWarning # noqa: E402
from .pin import Pin # noqa: E402
from ddtrace._trace.pin import Pin # noqa: E402
from ddtrace._trace.span import Span # noqa: E402
from ddtrace._trace.tracer import Tracer # noqa: E402
from ddtrace.vendor import debtcollector
Expand Down Expand Up @@ -67,6 +67,7 @@
_DEPRECATED_MODULE_ATTRIBUTES = [
"Span",
"Tracer",
"Pin",
]


Expand Down
72 changes: 72 additions & 0 deletions ddtrace/_trace/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import abc
import re
from typing import TYPE_CHECKING # noqa:F401
from typing import List # noqa:F401
from typing import Optional # noqa:F401
from typing import Union # noqa:F401

from ddtrace._trace.processor import TraceProcessor
from ddtrace.ext import http


if TYPE_CHECKING: # pragma: no cover
from ddtrace._trace.span import Span # noqa:F401


class TraceFilter(TraceProcessor):
@abc.abstractmethod
def process_trace(self, trace):
# type: (List[Span]) -> Optional[List[Span]]
"""Processes a trace.
None can be returned to prevent the trace from being exported.
"""
pass


class FilterRequestsOnUrl(TraceFilter):
r"""Filter out traces from incoming http requests based on the request's url.
This class takes as argument a list of regular expression patterns
representing the urls to be excluded from tracing. A trace will be excluded
if its root span contains a ``http.url`` tag and if this tag matches any of
the provided regular expression using the standard python regexp match
semantic (https://docs.python.org/3/library/re.html#re.match).
:param list regexps: a list of regular expressions (or a single string) defining
the urls that should be filtered out.
Examples:
To filter out http calls to domain api.example.com::
FilterRequestsOnUrl(r'http://api\\.example\\.com')
To filter out http calls to all first level subdomains from example.com::
FilterRequestOnUrl(r'http://.*+\\.example\\.com')
To filter out calls to both http://test.example.com and http://example.com/healthcheck::
FilterRequestOnUrl([r'http://test\\.example\\.com', r'http://example\\.com/healthcheck'])
"""

def __init__(self, regexps: Union[str, List[str]]):
if isinstance(regexps, str):
regexps = [regexps]
self._regexps = [re.compile(regexp) for regexp in regexps]

def process_trace(self, trace):
# type: (List[Span]) -> Optional[List[Span]]
"""
When the filter is registered in the tracer, process_trace is called by
on each trace before it is sent to the agent, the returned value will
be fed to the next filter in the list. If process_trace returns None,
the whole trace is discarded.
"""
for span in trace:
url = span.get_tag(http.URL)
if span.parent_id is None and url is not None:
for regexp in self._regexps:
if regexp.match(url):
return None
return trace
209 changes: 209 additions & 0 deletions ddtrace/_trace/pin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
from typing import TYPE_CHECKING # noqa:F401
from typing import Any # noqa:F401
from typing import Dict # noqa:F401
from typing import Optional # noqa:F401

import wrapt

import ddtrace

from ..internal.logger import get_logger


log = get_logger(__name__)


# To set attributes on wrapt proxy objects use this prefix:
# http://wrapt.readthedocs.io/en/latest/wrappers.html
_DD_PIN_NAME = "_datadog_pin"
_DD_PIN_PROXY_NAME = "_self_" + _DD_PIN_NAME


class Pin(object):
"""Pin (a.k.a Patch INfo) is a small class which is used to
set tracing metadata on a particular traced connection.
This is useful if you wanted to, say, trace two different
database clusters.
>>> conn = sqlite.connect('/tmp/user.db')
>>> # Override a pin for a specific connection
>>> pin = Pin.override(conn, service='user-db')
>>> conn = sqlite.connect('/tmp/image.db')
"""

__slots__ = ["tags", "tracer", "_target", "_config", "_initialized"]

def __init__(
self,
service=None, # type: Optional[str]
tags=None, # type: Optional[Dict[str, str]]
tracer=None,
_config=None, # type: Optional[Dict[str, Any]]
):
# type: (...) -> None
tracer = tracer or ddtrace.tracer
self.tags = tags
self.tracer = tracer
self._target = None # type: Optional[int]
# keep the configuration attribute internal because the
# public API to access it is not the Pin class
self._config = _config or {} # type: Dict[str, Any]
# [Backward compatibility]: service argument updates the `Pin` config
self._config["service_name"] = service
self._initialized = True

@property
def service(self):
# type: () -> str
"""Backward compatibility: accessing to `pin.service` returns the underlying
configuration value.
"""
return self._config["service_name"]

def __setattr__(self, name, value):
if getattr(self, "_initialized", False) and name != "_target":
raise AttributeError("can't mutate a pin, use override() or clone() instead")
super(Pin, self).__setattr__(name, value)

def __repr__(self):
return "Pin(service=%s, tags=%s, tracer=%s)" % (self.service, self.tags, self.tracer)

@staticmethod
def _find(*objs):
# type: (Any) -> Optional[Pin]
"""
Return the first :class:`ddtrace.trace.Pin` found on any of the provided objects or `None` if none were found
>>> pin = Pin._find(wrapper, instance, conn)
:param objs: The objects to search for a :class:`ddtrace.trace.Pin` on
:type objs: List of objects
:rtype: :class:`ddtrace.trace.Pin`, None
:returns: The first found :class:`ddtrace.trace.Pin` or `None` is none was found
"""
for obj in objs:
pin = Pin.get_from(obj)
if pin:
return pin
return None

@staticmethod
def get_from(obj):
# type: (Any) -> Optional[Pin]
"""Return the pin associated with the given object. If a pin is attached to
`obj` but the instance is not the owner of the pin, a new pin is cloned and
attached. This ensures that a pin inherited from a class is a copy for the new
instance, avoiding that a specific instance overrides other pins values.
>>> pin = Pin.get_from(conn)
:param obj: The object to look for a :class:`ddtrace.trace.Pin` on
:type obj: object
:rtype: :class:`ddtrace.trace.Pin`, None
:returns: :class:`ddtrace.trace.Pin` associated with the object or None
"""
if hasattr(obj, "__getddpin__"):
return obj.__getddpin__()

pin_name = _DD_PIN_PROXY_NAME if isinstance(obj, wrapt.ObjectProxy) else _DD_PIN_NAME
pin = getattr(obj, pin_name, None)
# detect if the PIN has been inherited from a class
if pin is not None and pin._target != id(obj):
pin = pin.clone()
pin.onto(obj)
return pin

@classmethod
def override(
cls,
obj, # type: Any
service=None, # type: Optional[str]
tags=None, # type: Optional[Dict[str, str]]
tracer=None,
):
# type: (...) -> None
"""Override an object with the given attributes.
That's the recommended way to customize an already instrumented client, without
losing existing attributes.
>>> conn = sqlite.connect('/tmp/user.db')
>>> # Override a pin for a specific connection
>>> Pin.override(conn, service='user-db')
"""
if not obj:
return

pin = cls.get_from(obj)
if pin is None:
Pin(service=service, tags=tags, tracer=tracer).onto(obj)
else:
pin.clone(service=service, tags=tags, tracer=tracer).onto(obj)

def enabled(self):
# type: () -> bool
"""Return true if this pin's tracer is enabled."""
# inline to avoid circular imports
from ddtrace.settings.asm import config as asm_config

return bool(self.tracer) and (self.tracer.enabled or asm_config._apm_opt_out)

def onto(self, obj, send=True):
# type: (Any, bool) -> None
"""Patch this pin onto the given object. If send is true, it will also
queue the metadata to be sent to the server.
"""
# Actually patch it on the object.
try:
if hasattr(obj, "__setddpin__"):
return obj.__setddpin__(self)

pin_name = _DD_PIN_PROXY_NAME if isinstance(obj, wrapt.ObjectProxy) else _DD_PIN_NAME

# set the target reference; any get_from, clones and retarget the new PIN
self._target = id(obj)
if self.service:
ddtrace.config._add_extra_service(self.service)
return setattr(obj, pin_name, self)
except AttributeError:
log.debug("can't pin onto object. skipping", exc_info=True)

def remove_from(self, obj):
# type: (Any) -> None
# Remove pin from the object.
try:
pin_name = _DD_PIN_PROXY_NAME if isinstance(obj, wrapt.ObjectProxy) else _DD_PIN_NAME

pin = Pin.get_from(obj)
if pin is not None:
delattr(obj, pin_name)
except AttributeError:
log.debug("can't remove pin from object. skipping", exc_info=True)

def clone(
self,
service=None, # type: Optional[str]
tags=None, # type: Optional[Dict[str, str]]
tracer=None,
):
# type: (...) -> Pin
"""Return a clone of the pin with the given attributes replaced."""
# do a shallow copy of Pin dicts
if not tags and self.tags:
tags = self.tags.copy()

# we use a copy instead of a deepcopy because we expect configurations
# to have only a root level dictionary without nested objects. Using
# deepcopy introduces a big overhead:
#
# copy: 0.00654911994934082
# deepcopy: 0.2787208557128906
config = self._config.copy()

return Pin(
service=service or self.service,
tags=tags,
tracer=tracer or self.tracer, # do not clone the Tracer
_config=config,
)
2 changes: 1 addition & 1 deletion ddtrace/_trace/tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
from ddtrace.constants import HOSTNAME_KEY
from ddtrace.constants import PID
from ddtrace.constants import VERSION_KEY
from ddtrace.filters import TraceFilter
from ddtrace.internal import agent
from ddtrace.internal import atexit
from ddtrace.internal import compat
Expand Down Expand Up @@ -69,6 +68,7 @@
from ddtrace.settings import Config
from ddtrace.settings.asm import config as asm_config
from ddtrace.settings.peer_service import _ps_config
from ddtrace.trace import TraceFilter
from ddtrace.vendor.debtcollector import deprecate


Expand Down
2 changes: 1 addition & 1 deletion ddtrace/contrib/aiomysql/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
To configure the integration on an per-connection basis use the
``Pin`` API::
from ddtrace import Pin
from ddtrace.trace import Pin
import asyncio
import aiomysql
Expand Down
3 changes: 2 additions & 1 deletion ddtrace/contrib/aiopg/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""
Instrument aiopg to report a span for each executed Postgres queries::
from ddtrace import Pin, patch
from ddtrace import patch
from ddtrace.trace import Pin
import aiopg
# If not patched yet, you can patch aiopg specifically
Expand Down
2 changes: 1 addition & 1 deletion ddtrace/contrib/aioredis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
``Pin`` API::
import aioredis
from ddtrace import Pin
from ddtrace.trace import Pin
myaioredis = aioredis.Aioredis()
Pin.override(myaioredis, service="myaioredis")
Expand Down
3 changes: 2 additions & 1 deletion ddtrace/contrib/anthropic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@
``Pin`` API::
import anthropic
from ddtrace import Pin, config
from ddtrace import config
from ddtrace.trace import Pin
Pin.override(anthropic, service="my-anthropic-service")
""" # noqa: E501
Expand Down
2 changes: 1 addition & 1 deletion ddtrace/contrib/aredis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
To configure particular aredis instances use the :class:`Pin <ddtrace.Pin>` API::
import aredis
from ddtrace import Pin
from ddtrace.trace import Pin
client = aredis.StrictRedis(host="localhost", port=6379)
Expand Down
2 changes: 1 addition & 1 deletion ddtrace/contrib/asyncpg/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
basis use the ``Pin`` API::
import asyncpg
from ddtrace import Pin
from ddtrace.trace import Pin
conn = asyncpg.connect("postgres://localhost:5432")
Pin.override(conn, service="custom-service")
Expand Down
3 changes: 2 additions & 1 deletion ddtrace/contrib/cassandra/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
``import ddtrace.auto`` will automatically patch your Cluster instance to make it work.
::
from ddtrace import Pin, patch
from ddtrace import patch
from ddtrace.trace import Pin
from cassandra.cluster import Cluster
# If not patched yet, you can patch cassandra specifically
Expand Down
Loading

0 comments on commit 5840592

Please sign in to comment.