Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 118 additions & 45 deletions agentops/instrumentation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
"""

from typing import Optional, Set, TypedDict

try:
from typing import NotRequired
except ImportError:
from typing_extensions import NotRequired

Check warning on line 20 in agentops/instrumentation/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/__init__.py#L19-L20

Added lines #L19 - L20 were not covered by tests
from types import ModuleType
from dataclasses import dataclass
import importlib
Expand All @@ -36,8 +41,11 @@

def _is_package_instrumented(package_name: str) -> bool:
"""Check if a package is already instrumented by looking at active instrumentors."""
# Handle package.module names by converting dots to underscores for comparison
normalized_name = package_name.replace(".", "_").lower()
return any(
instrumentor.__class__.__name__.lower().startswith(package_name.lower())
instrumentor.__class__.__name__.lower().startswith(normalized_name)
or instrumentor.__class__.__name__.lower().startswith(package_name.split(".")[-1].lower())
for instrumentor in _active_instrumentors
)

Expand Down Expand Up @@ -65,17 +73,14 @@
if package_name in AGENTIC_LIBRARIES:
_uninstrument_providers()
_has_agentic_library = True
logger.debug(f"Uninstrumented all providers due to agentic library {package_name} detection")
return True

# Skip providers if an agentic library is already instrumented
if package_name in PROVIDERS and _has_agentic_library:
logger.debug(f"Skipping provider {package_name} instrumentation as an agentic library is already instrumented")
return False

# Skip if already instrumented
if _is_package_instrumented(package_name):
logger.debug(f"Package {package_name} is already instrumented")
return False

return True
Expand All @@ -102,36 +107,62 @@
Monitor imports and instrument packages as they are imported.
This replaces the built-in import function to intercept package imports.
"""
global _instrumenting_packages
root = name.split(".", 1)[0]
global _instrumenting_packages, _has_agentic_library

# Skip providers if an agentic library is already instrumented
if _has_agentic_library and root in PROVIDERS:
# If an agentic library is already instrumented, skip all further instrumentation
if _has_agentic_library:
return _original_builtins_import(name, globals_dict, locals_dict, fromlist, level)

# Check if this is a package we should instrument
if (
root in TARGET_PACKAGES
and root not in _instrumenting_packages
and not _is_package_instrumented(root) # Check if already instrumented before adding
):
logger.debug(f"Detected import of {root}")
_instrumenting_packages.add(root)
try:
_perform_instrumentation(root)
except Exception as e:
logger.error(f"Error instrumenting {root}: {str(e)}")
finally:
_instrumenting_packages.discard(root)

return _original_builtins_import(name, globals_dict, locals_dict, fromlist, level)
# First, do the actual import
module = _original_builtins_import(name, globals_dict, locals_dict, fromlist, level)

# Check for exact matches first (handles package.module like google.adk)
packages_to_check = set()

# Check the imported module itself
if name in TARGET_PACKAGES:
packages_to_check.add(name)

Check warning on line 124 in agentops/instrumentation/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/__init__.py#L124

Added line #L124 was not covered by tests
else:
# Check if any target package is a prefix of the import name
for target in TARGET_PACKAGES:
if name.startswith(target + ".") or name == target:
packages_to_check.add(target)

# For "from X import Y" style imports, also check submodules
if fromlist:
for item in fromlist:
full_name = f"{name}.{item}"
if full_name in TARGET_PACKAGES:
packages_to_check.add(full_name)

Check warning on line 136 in agentops/instrumentation/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/__init__.py#L136

Added line #L136 was not covered by tests
else:
# Check if any target package matches this submodule
for target in TARGET_PACKAGES:
if full_name == target or full_name.startswith(target + "."):
packages_to_check.add(target)

Check warning on line 141 in agentops/instrumentation/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/__init__.py#L141

Added line #L141 was not covered by tests

# Instrument all matching packages
for package_to_check in packages_to_check:
if package_to_check not in _instrumenting_packages and not _is_package_instrumented(package_to_check):
_instrumenting_packages.add(package_to_check)
try:
_perform_instrumentation(package_to_check)

Check warning on line 148 in agentops/instrumentation/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/__init__.py#L146-L148

Added lines #L146 - L148 were not covered by tests
# If we just instrumented an agentic library, stop
if _has_agentic_library:
break
except Exception as e:
logger.error(f"Error instrumenting {package_to_check}: {str(e)}")

Check warning on line 153 in agentops/instrumentation/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/__init__.py#L150-L153

Added lines #L150 - L153 were not covered by tests
finally:
_instrumenting_packages.discard(package_to_check)

Check warning on line 155 in agentops/instrumentation/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/__init__.py#L155

Added line #L155 was not covered by tests

return module


# Define the structure for instrumentor configurations
class InstrumentorConfig(TypedDict):
module_name: str
class_name: str
min_version: str
package_name: NotRequired[str] # Optional: actual pip package name if different from module


# Configuration for supported LLM providers
Expand All @@ -146,16 +177,17 @@
"class_name": "AnthropicInstrumentor",
"min_version": "0.32.0",
},
"google.genai": {
"module_name": "agentops.instrumentation.google_generativeai",
"class_name": "GoogleGenerativeAIInstrumentor",
"min_version": "0.1.0",
},
"ibm_watsonx_ai": {
"module_name": "agentops.instrumentation.ibm_watsonx_ai",
"class_name": "IBMWatsonXInstrumentor",
"min_version": "0.1.0",
},
"google.genai": {
"module_name": "agentops.instrumentation.google_generativeai",
"class_name": "GoogleGenerativeAIInstrumentor",
"min_version": "0.1.0",
"package_name": "google-genai", # Actual pip package name
},
}

# Configuration for supported agentic libraries
Expand All @@ -171,6 +203,11 @@
"class_name": "OpenAIAgentsInstrumentor",
"min_version": "0.0.1",
},
"google.adk": {
"module_name": "agentops.instrumentation.google_adk",
"class_name": "GoogleADKInstrumentor",
"min_version": "0.1.0",
},
}

# Combine all target packages for monitoring
Expand All @@ -190,6 +227,7 @@
module_name: str
class_name: str
min_version: str
package_name: Optional[str] = None # Optional: actual pip package name

@property
def module(self) -> ModuleType:
Expand All @@ -200,7 +238,11 @@
def should_activate(self) -> bool:
"""Check if the package is available and meets version requirements."""
try:
provider_name = self.module_name.split(".")[-1]
# Use explicit package_name if provided, otherwise derive from module_name
if self.package_name:
provider_name = self.package_name

Check warning on line 243 in agentops/instrumentation/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/__init__.py#L243

Added line #L243 was not covered by tests
else:
provider_name = self.module_name.split(".")[-1]
module_version = version(provider_name)
return module_version is not None and Version(module_version) >= parse(self.min_version)
except ImportError:
Expand Down Expand Up @@ -233,24 +275,44 @@
# Check if active_instrumentors is empty, as a proxy for not started.
if not _active_instrumentors:
builtins.__import__ = _import_monitor
global _instrumenting_packages
global _instrumenting_packages, _has_agentic_library

# If an agentic library is already instrumented, don't instrument anything else
if _has_agentic_library:
return

Check warning on line 282 in agentops/instrumentation/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/__init__.py#L282

Added line #L282 was not covered by tests

for name in list(sys.modules.keys()):
# Stop if an agentic library gets instrumented during the loop
if _has_agentic_library:
break

module = sys.modules.get(name)
if not isinstance(module, ModuleType):
continue

root = name.split(".", 1)[0]
if _has_agentic_library and root in PROVIDERS:
continue

if root in TARGET_PACKAGES and root not in _instrumenting_packages and not _is_package_instrumented(root):
_instrumenting_packages.add(root)
# Check for exact matches first (handles package.module like google.adk)
package_to_check = None
if name in TARGET_PACKAGES:
package_to_check = name
else:
# Check if any target package is a prefix of the module name
for target in TARGET_PACKAGES:
if name.startswith(target + ".") or name == target:
package_to_check = target
break

if (
package_to_check
and package_to_check not in _instrumenting_packages
and not _is_package_instrumented(package_to_check)
):
_instrumenting_packages.add(package_to_check)
try:
_perform_instrumentation(root)
_perform_instrumentation(package_to_check)
except Exception as e:
logger.error(f"Error instrumenting {root}: {str(e)}")
logger.error(f"Error instrumenting {package_to_check}: {str(e)}")

Check warning on line 313 in agentops/instrumentation/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/__init__.py#L313

Added line #L313 was not covered by tests
finally:
_instrumenting_packages.discard(root)
_instrumenting_packages.discard(package_to_check)


def uninstrument_all():
Expand All @@ -269,8 +331,19 @@
Get all actively used libraries in the current execution context.
Returns a set of package names that are currently imported and being monitored.
"""
return {
name.split(".")[0]
for name, module in sys.modules.items()
if isinstance(module, ModuleType) and name.split(".")[0] in TARGET_PACKAGES
}
active_libs = set()
for name, module in sys.modules.items():
if not isinstance(module, ModuleType):
continue

Check warning on line 337 in agentops/instrumentation/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/__init__.py#L334-L337

Added lines #L334 - L337 were not covered by tests

# Check for exact matches first
if name in TARGET_PACKAGES:
active_libs.add(name)

Check warning on line 341 in agentops/instrumentation/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/__init__.py#L340-L341

Added lines #L340 - L341 were not covered by tests
else:
# Check if any target package is a prefix of the module name
for target in TARGET_PACKAGES:
if name.startswith(target + ".") or name == target:
active_libs.add(target)
break

Check warning on line 347 in agentops/instrumentation/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/__init__.py#L344-L347

Added lines #L344 - L347 were not covered by tests

return active_libs

Check warning on line 349 in agentops/instrumentation/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/__init__.py#L349

Added line #L349 was not covered by tests
20 changes: 20 additions & 0 deletions agentops/instrumentation/google_adk/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""Google ADK Instrumentation for AgentOps

This module provides instrumentation for Google's Agent Development Kit (ADK),
capturing agent execution, LLM calls, tool calls, and other ADK-specific events.
"""

from importlib.metadata import version, PackageNotFoundError

Check warning on line 7 in agentops/instrumentation/google_adk/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/google_adk/__init__.py#L7

Added line #L7 was not covered by tests

try:
__version__ = version("google-adk")
except PackageNotFoundError:
__version__ = "0.0.0"

Check warning on line 12 in agentops/instrumentation/google_adk/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/google_adk/__init__.py#L9-L12

Added lines #L9 - L12 were not covered by tests

LIBRARY_NAME = "agentops.instrumentation.google_adk"
LIBRARY_VERSION = __version__

Check warning on line 15 in agentops/instrumentation/google_adk/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/google_adk/__init__.py#L14-L15

Added lines #L14 - L15 were not covered by tests

from agentops.instrumentation.google_adk.instrumentor import GoogleADKInstrumentor # noqa: E402
from agentops.instrumentation.google_adk import patch # noqa: E402

Check warning on line 18 in agentops/instrumentation/google_adk/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/google_adk/__init__.py#L17-L18

Added lines #L17 - L18 were not covered by tests

__all__ = ["LIBRARY_NAME", "LIBRARY_VERSION", "GoogleADKInstrumentor", "patch"]

Check warning on line 20 in agentops/instrumentation/google_adk/__init__.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/google_adk/__init__.py#L20

Added line #L20 was not covered by tests
78 changes: 78 additions & 0 deletions agentops/instrumentation/google_adk/instrumentor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Google ADK Instrumentation for AgentOps

This module provides instrumentation for Google's Agent Development Kit (ADK).
It uses a patching approach to:
1. Disable ADK's built-in telemetry to prevent duplicate spans
2. Create AgentOps spans that mirror ADK's telemetry structure
3. Extract and properly index LLM messages and tool calls
"""

from typing import Collection
from opentelemetry.trace import get_tracer
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
from opentelemetry.metrics import get_meter

Check warning on line 13 in agentops/instrumentation/google_adk/instrumentor.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/google_adk/instrumentor.py#L10-L13

Added lines #L10 - L13 were not covered by tests

from agentops.logging import logger
from agentops.instrumentation.google_adk import LIBRARY_NAME, LIBRARY_VERSION
from agentops.instrumentation.google_adk.patch import patch_adk, unpatch_adk
from agentops.semconv import Meters

Check warning on line 18 in agentops/instrumentation/google_adk/instrumentor.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/google_adk/instrumentor.py#L15-L18

Added lines #L15 - L18 were not covered by tests


class GoogleADKInstrumentor(BaseInstrumentor):

Check warning on line 21 in agentops/instrumentation/google_adk/instrumentor.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/google_adk/instrumentor.py#L21

Added line #L21 was not covered by tests
"""An instrumentor for Google Agent Development Kit (ADK).

This instrumentor patches Google ADK to:
- Prevent ADK from creating its own telemetry spans
- Create AgentOps spans for agent runs, LLM calls, and tool calls
- Properly extract and index message content and tool interactions
"""

def instrumentation_dependencies(self) -> Collection[str]:

Check warning on line 30 in agentops/instrumentation/google_adk/instrumentor.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/google_adk/instrumentor.py#L30

Added line #L30 was not covered by tests
"""Return packages required for instrumentation."""
return ["google-adk >= 0.1.0"]

Check warning on line 32 in agentops/instrumentation/google_adk/instrumentor.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/google_adk/instrumentor.py#L32

Added line #L32 was not covered by tests

def _instrument(self, **kwargs):

Check warning on line 34 in agentops/instrumentation/google_adk/instrumentor.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/google_adk/instrumentor.py#L34

Added line #L34 was not covered by tests
"""Instrument the Google ADK.

This method:
1. Disables ADK's built-in telemetry
2. Patches key ADK methods to create AgentOps spans
3. Sets up metrics for tracking token usage and operation duration
"""
# Set up tracer and meter
tracer_provider = kwargs.get("tracer_provider")
tracer = get_tracer(LIBRARY_NAME, LIBRARY_VERSION, tracer_provider)

Check warning on line 44 in agentops/instrumentation/google_adk/instrumentor.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/google_adk/instrumentor.py#L43-L44

Added lines #L43 - L44 were not covered by tests

meter_provider = kwargs.get("meter_provider")
meter = get_meter(LIBRARY_NAME, LIBRARY_VERSION, meter_provider)

Check warning on line 47 in agentops/instrumentation/google_adk/instrumentor.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/google_adk/instrumentor.py#L46-L47

Added lines #L46 - L47 were not covered by tests

# Create metrics
meter.create_histogram(

Check warning on line 50 in agentops/instrumentation/google_adk/instrumentor.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/google_adk/instrumentor.py#L50

Added line #L50 was not covered by tests
name=Meters.LLM_TOKEN_USAGE,
unit="token",
description="Measures number of input and output tokens used with Google ADK",
)

meter.create_histogram(

Check warning on line 56 in agentops/instrumentation/google_adk/instrumentor.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/google_adk/instrumentor.py#L56

Added line #L56 was not covered by tests
name=Meters.LLM_OPERATION_DURATION,
unit="s",
description="Google ADK operation duration",
)

meter.create_counter(

Check warning on line 62 in agentops/instrumentation/google_adk/instrumentor.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/google_adk/instrumentor.py#L62

Added line #L62 was not covered by tests
name=Meters.LLM_COMPLETIONS_EXCEPTIONS,
unit="time",
description="Number of exceptions occurred during Google ADK operations",
)

# Apply patches
patch_adk(tracer)
logger.info("Google ADK instrumentation enabled")

Check warning on line 70 in agentops/instrumentation/google_adk/instrumentor.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/google_adk/instrumentor.py#L69-L70

Added lines #L69 - L70 were not covered by tests

def _uninstrument(self, **kwargs):

Check warning on line 72 in agentops/instrumentation/google_adk/instrumentor.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/google_adk/instrumentor.py#L72

Added line #L72 was not covered by tests
"""Remove instrumentation from Google ADK.

This method removes all patches and restores ADK's original behavior.
"""
unpatch_adk()
logger.info("Google ADK instrumentation disabled")

Check warning on line 78 in agentops/instrumentation/google_adk/instrumentor.py

View check run for this annotation

Codecov / codecov/patch

agentops/instrumentation/google_adk/instrumentor.py#L77-L78

Added lines #L77 - L78 were not covered by tests
Loading
Loading