Files
2020-04-08 10:39:44 -07:00

185 lines
6.6 KiB
Python

import ddtrace
from ddtrace.vendor import debtcollector
from .internal.logger import get_logger
from .vendor import wrapt
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__ = ['app', 'tags', 'tracer', '_target', '_config', '_initialized']
@debtcollector.removals.removed_kwarg("app_type")
def __init__(self, service, app=None, app_type=None, tags=None, tracer=None, _config=None):
tracer = tracer or ddtrace.tracer
self.app = app
self.tags = tags
self.tracer = tracer
self._target = None
# keep the configuration attribute internal because the
# public API to access it is not the Pin class
self._config = _config or {}
# [Backward compatibility]: service argument updates the `Pin` config
self._config['service_name'] = service
self._initialized = True
@property
def service(self):
"""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, app=%s, tags=%s, tracer=%s)' % (
self.service, self.app, self.tags, self.tracer)
@staticmethod
def _find(*objs):
"""
Return the first :class:`ddtrace.pin.Pin` found on any of the provided objects or `None` if none were found
>>> pin = Pin._find(wrapper, instance, conn, app)
:param objs: The objects to search for a :class:`ddtrace.pin.Pin` on
:type objs: List of objects
:rtype: :class:`ddtrace.pin.Pin`, None
:returns: The first found :class:`ddtrace.pin.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):
"""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.pin.Pin` on
:type obj: object
:rtype: :class:`ddtrace.pin.Pin`, None
:returns: :class:`ddtrace.pin.Pin` associated with the object, or None if none was found
"""
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
@debtcollector.removals.removed_kwarg("app_type")
def override(cls, obj, service=None, app=None, app_type=None, tags=None, tracer=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 not pin:
pin = Pin(service)
pin.clone(
service=service,
app=app,
tags=tags,
tracer=tracer,
).onto(obj)
def enabled(self):
"""Return true if this pin's tracer is enabled. """
return bool(self.tracer) and self.tracer.enabled
def onto(self, obj, send=True):
"""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)
return setattr(obj, pin_name, self)
except AttributeError:
log.debug("can't pin onto object. skipping", exc_info=True)
def remove_from(self, obj):
# 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)
@debtcollector.removals.removed_kwarg("app_type")
def clone(self, service=None, app=None, app_type=None, tags=None, tracer=None):
"""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,
app=app or self.app,
tags=tags,
tracer=tracer or self.tracer, # do not clone the Tracer
_config=config,
)