11import logging
2- import wrapt
32
3+ import wrapt
44import ddtrace
55
6+
67log = logging .getLogger (__name__ )
78
8- _DD_PIN_NAME = '_datadog_pin'
99
1010# To set attributes on wrapt proxy objects use this prefix:
1111# http://wrapt.readthedocs.io/en/latest/wrappers.html
12+ _DD_PIN_NAME = '_datadog_pin'
1213_DD_PIN_PROXY_NAME = '_self_' + _DD_PIN_NAME
1314
1415
1516class Pin (object ):
16- """ Pin (a.k.a Patch INfo) is a small class which is used to
17- set tracing metadata on a particular traced connection.
18- This is useful if you wanted to, say, trace two different
19- database clusters.
17+ """Pin (a.k.a Patch INfo) is a small class which is used to
18+ set tracing metadata on a particular traced connection.
19+ This is useful if you wanted to, say, trace two different
20+ database clusters.
2021
2122 >>> conn = sqlite.connect("/tmp/user.db")
2223 >>> # Override a pin for a specific connection
2324 >>> pin = Pin.override(conn, service="user-db")
2425 >>> conn = sqlite.connect("/tmp/image.db")
2526 """
27+ __slots__ = ['app' , 'app_type' , 'tags' , 'tracer' , '_target' , '_config' , '_initialized' ]
2628
27- __slots__ = ['app' , 'app_type' , 'service' , 'tags' , 'tracer' , '_initialized' ]
28-
29- def __init__ (self , service , app = None , app_type = None , tags = None , tracer = None ):
29+ def __init__ (self , service , app = None , app_type = None , tags = None , tracer = None , _config = None ):
3030 tracer = tracer or ddtrace .tracer
31- self .service = service
3231 self .app = app
3332 self .app_type = app_type
3433 self .tags = tags
3534 self .tracer = tracer
35+ self ._target = None
36+ # keep the configuration attribute internal because the
37+ # public API to access it is not the Pin class
38+ self ._config = _config or {}
39+ # [Backward compatibility]: service argument updates the `Pin` config
40+ self ._config ['service_name' ] = service
3641 self ._initialized = True
3742
43+ @property
44+ def service (self ):
45+ """Backward compatibility: accessing to `pin.service` returns the underlying
46+ configuration value.
47+ """
48+ return self ._config ['service_name' ]
49+
3850 def __setattr__ (self , name , value ):
39- if hasattr (self , '_initialized' ) :
51+ if getattr (self , '_initialized' , False ) and name is not '_target' :
4052 raise AttributeError ("can't mutate a pin, use override() or clone() instead" )
4153 super (Pin , self ).__setattr__ (name , value )
4254
@@ -46,15 +58,23 @@ def __repr__(self):
4658
4759 @staticmethod
4860 def get_from (obj ):
49- """ Return the pin associated with the given object.
61+ """Return the pin associated with the given object. If a pin is attached to
62+ `obj` but the instance is not the owner of the pin, a new pin is cloned and
63+ attached. This ensures that a pin inherited from a class is a copy for the new
64+ instance, avoiding that a specific instance overrides other pins values.
5065
5166 >>> pin = Pin.get_from(conn)
5267 """
5368 if hasattr (obj , '__getddpin__' ):
5469 return obj .__getddpin__ ()
5570
5671 pin_name = _DD_PIN_PROXY_NAME if isinstance (obj , wrapt .ObjectProxy ) else _DD_PIN_NAME
57- return getattr (obj , pin_name , None )
72+ pin = getattr (obj , pin_name , None )
73+ # detect if the PIN has been inherited from a class
74+ if pin is not None and pin ._target != id (obj ):
75+ pin = pin .clone ()
76+ pin .onto (obj )
77+ return pin
5878
5979 @classmethod
6080 def override (cls , obj , service = None , app = None , app_type = None , tags = None , tracer = None ):
@@ -63,9 +83,9 @@ def override(cls, obj, service=None, app=None, app_type=None, tags=None, tracer=
6383 That's the recommended way to customize an already instrumented client, without
6484 losing existing attributes.
6585
66- >>> conn = sqlite.connect("/tmp/user.db")
67- >>> # Override a pin for a specific connection
68- >>> pin = Pin.override(conn, service="user-db")
86+ >>> conn = sqlite.connect("/tmp/user.db")
87+ >>> # Override a pin for a specific connection
88+ >>> Pin.override(conn, service="user-db")
6989 """
7090 if not obj :
7191 return
@@ -79,15 +99,16 @@ def override(cls, obj, service=None, app=None, app_type=None, tags=None, tracer=
7999 app = app ,
80100 app_type = app_type ,
81101 tags = tags ,
82- tracer = tracer ).onto (obj )
102+ tracer = tracer ,
103+ ).onto (obj )
83104
84105 def enabled (self ):
85- """ Return true if this pin's tracer is enabled. """
106+ """Return true if this pin's tracer is enabled. """
86107 return bool (self .tracer ) and self .tracer .enabled
87108
88109 def onto (self , obj , send = True ):
89- """ Patch this pin onto the given object. If send is true, it will also
90- queue the metadata to be sent to the server.
110+ """Patch this pin onto the given object. If send is true, it will also
111+ queue the metadata to be sent to the server.
91112 """
92113 # pinning will also queue the metadata for service submission. this
93114 # feels a bit side-effecty, but bc it's async and pretty clearly
@@ -104,25 +125,39 @@ def onto(self, obj, send=True):
104125 return obj .__setddpin__ (self )
105126
106127 pin_name = _DD_PIN_PROXY_NAME if isinstance (obj , wrapt .ObjectProxy ) else _DD_PIN_NAME
128+
129+ # set the target reference; any get_from, clones and retarget the new PIN
130+ self ._target = id (obj )
107131 return setattr (obj , pin_name , self )
108132 except AttributeError :
109133 log .debug ("can't pin onto object. skipping" , exc_info = True )
110134
111135 def clone (self , service = None , app = None , app_type = None , tags = None , tracer = None ):
112- """ Return a clone of the pin with the given attributes replaced. """
136+ """Return a clone of the pin with the given attributes replaced."""
137+ # do a shallow copy of Pin dicts
113138 if not tags and self .tags :
114- # do a shallow copy of the tags if needed.
115- tags = {k :v for k , v in self .tags .items ()}
139+ tags = self .tags .copy ()
140+
141+ # we use a copy instead of a deepcopy because we expect configurations
142+ # to have only a root level dictionary without nested objects. Using
143+ # deepcopy introduces a big overhead:
144+ #
145+ # copy: 0.00654911994934082
146+ # deepcopy: 0.2787208557128906
147+ config = self ._config .copy ()
116148
117149 return Pin (
118150 service = service or self .service ,
119151 app = app or self .app ,
120152 app_type = app_type or self .app_type ,
121153 tags = tags ,
122- tracer = tracer or self .tracer ) # no copy of the tracer
154+ tracer = tracer or self .tracer , # do not clone the Tracer
155+ _config = config ,
156+ )
123157
124158 def _send (self ):
125159 self .tracer .set_service_info (
126160 service = self .service ,
127161 app = self .app ,
128- app_type = self .app_type )
162+ app_type = self .app_type ,
163+ )
0 commit comments