Skip to content

Commit 744ef47

Browse files
committed
Merge branch 'main' into add-concurrency-instrumentaion
2 parents 397760c + 3f71e28 commit 744ef47

File tree

2 files changed

+255
-47
lines changed

2 files changed

+255
-47
lines changed

agentops/instrumentation/__init__.py

Lines changed: 223 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
from packaging.version import Version, parse
2727
import builtins
2828

29+
# Add os and site for path checking
30+
import os
31+
import site
32+
2933
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor # type: ignore
3034

3135
from 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+
111154
def _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

122184
def _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

135211
def _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

163269
def _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

184327
def _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

Comments
 (0)