2626from packaging .version import Version , parse
2727import builtins
2828
29+ # Add os and site for path checking
30+ import os
31+ import site
32+
2933from opentelemetry .instrumentation .instrumentor import BaseInstrumentor # type: ignore
3034
3135from agentops .logging import logger
@@ -108,28 +112,100 @@ class InstrumentorConfig(TypedDict):
108112_has_agentic_library : bool = False
109113
110114
115+ # New helper function to check module origin
116+ def _is_installed_package (module_obj : ModuleType , package_name_key : str ) -> bool :
117+ """
118+ Determines if the given module object corresponds to an installed site-package
119+ rather than a local module, especially when names might collide.
120+ `package_name_key` is the key from TARGET_PACKAGES (e.g., 'agents', 'google.adk').
121+ """
122+ if not hasattr (module_obj , "__file__" ) or not module_obj .__file__ :
123+ logger .debug (
124+ f"_is_installed_package: Module '{ package_name_key } ' has no __file__, assuming it might be an SDK namespace package. Returning True."
125+ )
126+ return True
127+
128+ module_path = os .path .normcase (os .path .realpath (os .path .abspath (module_obj .__file__ )))
129+
130+ # Priority 1: Check if it's in any site-packages directory.
131+ site_packages_dirs = site .getsitepackages ()
132+ if isinstance (site_packages_dirs , str ):
133+ site_packages_dirs = [site_packages_dirs ]
134+
135+ if hasattr (site , "USER_SITE" ) and site .USER_SITE and os .path .exists (site .USER_SITE ):
136+ site_packages_dirs .append (site .USER_SITE )
137+
138+ normalized_site_packages_dirs = [
139+ os .path .normcase (os .path .realpath (p )) for p in site_packages_dirs if p and os .path .exists (p )
140+ ]
141+
142+ for sp_dir in normalized_site_packages_dirs :
143+ if module_path .startswith (sp_dir ):
144+ logger .debug (
145+ f"_is_installed_package: Module '{ package_name_key } ' is a library, instrumenting '{ package_name_key } '."
146+ )
147+ return True
148+
149+ # Priority 2: If not in site-packages, it's highly likely a local module or not an SDK we target.
150+ logger .debug (f"_is_installed_package: Module '{ package_name_key } ' is a local module, skipping instrumentation." )
151+ return False
152+
153+
111154def _is_package_instrumented (package_name : str ) -> bool :
112155 """Check if a package is already instrumented by looking at active instrumentors."""
113156 # Handle package.module names by converting dots to underscores for comparison
114- normalized_name = package_name .replace ("." , "_" ).lower ()
115- return any (
116- instrumentor .__class__ .__name__ .lower ().startswith (normalized_name )
117- or instrumentor .__class__ .__name__ .lower ().startswith (package_name .split ("." )[- 1 ].lower ())
118- for instrumentor in _active_instrumentors
119- )
157+ normalized_target_name = package_name .replace ("." , "_" ).lower ()
158+ for instrumentor in _active_instrumentors :
159+ # Check based on the key it was registered with
160+ if (
161+ hasattr (instrumentor , "_agentops_instrumented_package_key" )
162+ and instrumentor ._agentops_instrumented_package_key == package_name
163+ ):
164+ return True
165+
166+ # Fallback to class name check (existing logic, less precise)
167+ # We use split('.')[-1] for cases like 'google.genai' to match GenAIInstrumentor
168+ instrumentor_class_name_prefix = instrumentor .__class__ .__name__ .lower ().replace ("instrumentor" , "" )
169+ target_base_name = package_name .split ("." )[- 1 ].lower ()
170+ normalized_class_name_match = (
171+ normalized_target_name .startswith (instrumentor_class_name_prefix )
172+ or target_base_name == instrumentor_class_name_prefix
173+ )
174+
175+ if normalized_class_name_match :
176+ # This fallback can be noisy, let's make it more specific or rely on the key above more
177+ # For now, if the key matches or this broad name match works, consider instrumented.
178+ # This helps if _agentops_instrumented_package_key was somehow not set.
179+ return True
180+
181+ return False
120182
121183
122184def _uninstrument_providers ():
123185 """Uninstrument all provider instrumentors while keeping agentic libraries active."""
124186 global _active_instrumentors
125- providers_to_remove = []
187+ new_active_instrumentors = []
188+ uninstrumented_any = False
126189 for instrumentor in _active_instrumentors :
127- if any (instrumentor .__class__ .__name__ .lower ().startswith (provider .lower ()) for provider in PROVIDERS .keys ()):
128- instrumentor .uninstrument ()
129- logger .debug (f"Uninstrumented provider { instrumentor .__class__ .__name__ } " )
130- providers_to_remove .append (instrumentor )
190+ instrumented_key = getattr (instrumentor , "_agentops_instrumented_package_key" , None )
191+ if instrumented_key and instrumented_key in PROVIDERS :
192+ try :
193+ instrumentor .uninstrument ()
194+ logger .info (
195+ f"AgentOps: Uninstrumented provider: { instrumentor .__class__ .__name__ } (for package '{ instrumented_key } ') due to agentic library activation."
196+ )
197+ uninstrumented_any = True
198+ except Exception as e :
199+ logger .error (f"Error uninstrumenting provider { instrumentor .__class__ .__name__ } : { e } " )
200+ else :
201+ # Keep non-provider instrumentors or those without our key (shouldn't happen for managed ones)
202+ new_active_instrumentors .append (instrumentor )
131203
132- _active_instrumentors = [i for i in _active_instrumentors if i not in providers_to_remove ]
204+ if uninstrumented_any or not new_active_instrumentors and _active_instrumentors :
205+ logger .debug (
206+ f"_uninstrument_providers: Processed. Previous active: { len (_active_instrumentors )} , New active after filtering providers: { len (new_active_instrumentors )} "
207+ )
208+ _active_instrumentors = new_active_instrumentors
133209
134210
135211def _should_instrument_package (package_name : str ) -> bool :
@@ -139,30 +215,60 @@ def _should_instrument_package(package_name: str) -> bool:
139215 """
140216 global _has_agentic_library
141217
142- # If this is an agentic library, uninstrument all providers first
143- if package_name in AGENTIC_LIBRARIES :
144- _uninstrument_providers ()
145- _has_agentic_library = True
146- return True
147-
148- # Skip providers if an agentic library is already instrumented
149- if package_name in PROVIDERS and _has_agentic_library :
218+ # If already instrumented by AgentOps (using our refined check), skip.
219+ if _is_package_instrumented (package_name ):
220+ logger .debug (f"_should_instrument_package: '{ package_name } ' already instrumented by AgentOps. Skipping." )
150221 return False
151222
152223 # Utility instrumentors are always enabled regardless of agentic library state
153224 if package_name in UTILITY_INSTRUMENTORS :
154225 return not _is_package_instrumented (package_name )
155226
156- # Skip if already instrumented
157- if _is_package_instrumented (package_name ):
227+ is_target_agentic = package_name in AGENTIC_LIBRARIES
228+ is_target_provider = package_name in PROVIDERS
229+
230+ if not is_target_agentic and not is_target_provider :
231+ logger .debug (
232+ f"_should_instrument_package: '{ package_name } ' is not a targeted provider or agentic library. Skipping."
233+ )
158234 return False
159235
160- return True
236+ if _has_agentic_library :
237+ # An agentic library is already active.
238+ if is_target_agentic :
239+ logger .info (
240+ f"AgentOps: An agentic library is active. Skipping instrumentation for subsequent agentic library '{ package_name } '."
241+ )
242+ return False
243+ if is_target_provider :
244+ logger .info (
245+ f"AgentOps: An agentic library is active. Skipping instrumentation for provider '{ package_name } '."
246+ )
247+ return False
248+ else :
249+ # No agentic library is active yet.
250+ if is_target_agentic :
251+ logger .info (
252+ f"AgentOps: '{ package_name } ' is the first-targeted agentic library. Will uninstrument providers if any are/become active."
253+ )
254+ _uninstrument_providers ()
255+ _has_agentic_library = True
256+ return True
257+ if is_target_provider :
258+ logger .debug (
259+ f"_should_instrument_package: '{ package_name } ' is a provider, no agentic library active. Allowing."
260+ )
261+ return True
262+
263+ logger .debug (
264+ f"_should_instrument_package: Defaulting to False for '{ package_name } ' (state: _has_agentic_library={ _has_agentic_library } )"
265+ )
266+ return False
161267
162268
163269def _perform_instrumentation (package_name : str ):
164270 """Helper function to perform instrumentation for a given package."""
165- global _instrumenting_packages , _active_instrumentors
271+ global _instrumenting_packages , _active_instrumentors , _has_agentic_library
166272 if not _should_instrument_package (package_name ):
167273 return
168274
@@ -175,10 +281,47 @@ def _perform_instrumentation(package_name: str):
175281
176282 loader = InstrumentorLoader (** config )
177283
178- if loader .should_activate :
179- instrumentor = instrument_one (loader ) # instrument_one is already a module function
180- if instrumentor is not None :
181- _active_instrumentors .append (instrumentor )
284+ # instrument_one already checks loader.should_activate
285+ instrumentor_instance = instrument_one (loader )
286+ if instrumentor_instance is not None :
287+ # Check if it was *actually* instrumented by instrument_one by seeing if the instrument method was called successfully.
288+ # This relies on instrument_one returning None if its internal .instrument() call failed (if we revert that, this needs adjustment)
289+ # For now, assuming instrument_one returns instance only on full success.
290+ # User request was to return instrumentor even if .instrument() fails. So, we check if _agentops_instrumented_package_key was set by us.
291+
292+ # Let's assume instrument_one might return an instance whose .instrument() failed.
293+ # The key is set before _active_instrumentors.append, so if it's already there and matches, it means it's a re-attempt on the same package.
294+ # The _is_package_instrumented check at the start of _should_instrument_package should prevent most re-entry for the same package_name.
295+
296+ # Store the package key this instrumentor is for, to aid _is_package_instrumented
297+ instrumentor_instance ._agentops_instrumented_package_key = package_name
298+
299+ # Add to active_instrumentors only if it's not a duplicate in terms of package_key being instrumented
300+ # This is a safeguard, _is_package_instrumented should catch this earlier.
301+ is_newly_added = True
302+ for existing_inst in _active_instrumentors :
303+ if (
304+ hasattr (existing_inst , "_agentops_instrumented_package_key" )
305+ and existing_inst ._agentops_instrumented_package_key == package_name
306+ ):
307+ is_newly_added = False
308+ logger .debug (
309+ f"_perform_instrumentation: Instrumentor for '{ package_name } ' already in _active_instrumentors. Not adding again."
310+ )
311+ break
312+ if is_newly_added :
313+ _active_instrumentors .append (instrumentor_instance )
314+
315+ # If this was an agentic library AND it's newly effectively instrumented.
316+ if (
317+ package_name in AGENTIC_LIBRARIES and not _has_agentic_library
318+ ): # Check _has_agentic_library to ensure this is the *first* one.
319+ # _uninstrument_providers() was already called in _should_instrument_package for the first agentic library.
320+ _has_agentic_library = True
321+ else :
322+ logger .debug (
323+ f"_perform_instrumentation: instrument_one for '{ package_name } ' returned None. Not added to active instrumentors."
324+ )
182325
183326
184327def _import_monitor (name : str , globals_dict = None , locals_dict = None , fromlist = (), level = 0 ):
@@ -210,18 +353,39 @@ def _import_monitor(name: str, globals_dict=None, locals_dict=None, fromlist=(),
210353 # For "from X import Y" style imports, also check submodules
211354 if fromlist :
212355 for item in fromlist :
213- full_name = f"{ name } .{ item } "
214- if full_name in TARGET_PACKAGES :
215- packages_to_check .add (full_name )
216- else :
217- # Check if any target package matches this submodule
356+ # Construct potential full name, e.g., "google.adk" from name="google", item="adk"
357+ # Or if name="os", item="path", full_name="os.path"
358+ # If the original name itself is a multi-part name like "a.b", and item is "c", then "a.b.c"
359+ # This logic needs to correctly identify the root package if 'name' is already a sub-package.
360+ # The existing TARGET_PACKAGES check is simpler: it checks against pre-defined full names.
361+
362+ # Check full name if item forms part of a target package name
363+ full_item_name_candidate = f"{ name } .{ item } "
364+
365+ if full_item_name_candidate in TARGET_PACKAGES :
366+ packages_to_check .add (full_item_name_candidate )
367+ else : # Fallback to checking if 'name' itself is a target
218368 for target in TARGET_PACKAGES :
219- if full_name == target or full_name .startswith (target + "." ):
220- packages_to_check .add (target )
369+ if name == target or name .startswith (target + "." ):
370+ packages_to_check .add (target ) # Check the base target if a submodule is imported from it.
221371
222372 # Instrument all matching packages
223373 for package_to_check in packages_to_check :
224374 if package_to_check not in _instrumenting_packages and not _is_package_instrumented (package_to_check ):
375+ target_module_obj = sys .modules .get (package_to_check )
376+
377+ if target_module_obj :
378+ is_sdk = _is_installed_package (target_module_obj , package_to_check )
379+ if not is_sdk :
380+ logger .info (
381+ f"AgentOps: Target '{ package_to_check } ' appears to be a local module/directory. Skipping AgentOps SDK instrumentation for it."
382+ )
383+ continue
384+ else :
385+ logger .debug (
386+ f"_import_monitor: No module object found in sys.modules for '{ package_to_check } ', proceeding with SDK instrumentation attempt."
387+ )
388+
225389 _instrumenting_packages .add (package_to_check )
226390 try :
227391 _perform_instrumentation (package_to_check )
@@ -285,14 +449,23 @@ def instrument_one(loader: InstrumentorLoader) -> Optional[BaseInstrumentor]:
285449 Returns the instrumentor instance if successful, None otherwise.
286450 """
287451 if not loader .should_activate :
288- logger .debug (
289- f"Package { loader .module_name } not found or version < { loader .min_version } ; skipping instrumentation"
452+ # This log is important for users to know why something wasn't instrumented.
453+ logger .info (
454+ f"AgentOps: Package '{ loader .package_name or loader .module_name } ' not found or version is less than minimum required ('{ loader .min_version } '). Skipping instrumentation."
290455 )
291456 return None
292457
293458 instrumentor = loader .get_instance ()
294- instrumentor .instrument (tracer_provider = tracer .provider )
295- logger .debug (f"Instrumented { loader .class_name } " )
459+ try :
460+ instrumentor .instrument (tracer_provider = TracingCore .get_instance ()._provider )
461+ logger .info (
462+ f"AgentOps: Successfully instrumented '{ loader .class_name } ' for package '{ loader .package_name or loader .module_name } '."
463+ )
464+ except Exception as e :
465+ logger .error (
466+ f"Failed to instrument { loader .class_name } for { loader .package_name or loader .module_name } : { e } " ,
467+ exc_info = True ,
468+ )
296469 return instrumentor
297470
298471
@@ -332,6 +505,17 @@ def instrument_all():
332505 and package_to_check not in _instrumenting_packages
333506 and not _is_package_instrumented (package_to_check )
334507 ):
508+ target_module_obj = sys .modules .get (package_to_check )
509+
510+ if target_module_obj :
511+ is_sdk = _is_installed_package (target_module_obj , package_to_check )
512+ if not is_sdk :
513+ continue
514+ else :
515+ logger .debug (
516+ f"instrument_all: No module object found for '{ package_to_check } ' in sys.modules during startup scan. Proceeding cautiously."
517+ )
518+
335519 _instrumenting_packages .add (package_to_check )
336520 try :
337521 _perform_instrumentation (package_to_check )
0 commit comments