Skip to content

Commit d81108d

Browse files
fenilfalduDwij1704
andauthored
Implement concurrent.futures instrumentation for OpenTelemetry contex… (#1018)
* Implement concurrent.futures instrumentation for OpenTelemetry context propagation * ruff checks :) * ruff check again :( * damn ruff +_+ * heck ruff again * constants update * refactor the instrumentation with utlity instrumentation * added types to the function/methods defs --------- Co-authored-by: Dwij <96073160+Dwij1704@users.noreply.github.com>
1 parent 8dbcfa9 commit d81108d

File tree

4 files changed

+754
-73
lines changed

4 files changed

+754
-73
lines changed

agentops/instrumentation/__init__.py

Lines changed: 102 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,76 @@
3535
from agentops.logging import logger
3636
from agentops.sdk.core import tracer
3737

38+
39+
# Define the structure for instrumentor configurations
40+
class InstrumentorConfig(TypedDict):
41+
module_name: str
42+
class_name: str
43+
min_version: str
44+
package_name: NotRequired[str] # Optional: actual pip package name if different from module
45+
46+
47+
# Configuration for supported LLM providers
48+
PROVIDERS: dict[str, InstrumentorConfig] = {
49+
"openai": {
50+
"module_name": "agentops.instrumentation.openai",
51+
"class_name": "OpenAIInstrumentor",
52+
"min_version": "1.0.0",
53+
},
54+
"anthropic": {
55+
"module_name": "agentops.instrumentation.anthropic",
56+
"class_name": "AnthropicInstrumentor",
57+
"min_version": "0.32.0",
58+
},
59+
"ibm_watsonx_ai": {
60+
"module_name": "agentops.instrumentation.ibm_watsonx_ai",
61+
"class_name": "IBMWatsonXInstrumentor",
62+
"min_version": "0.1.0",
63+
},
64+
"google.genai": {
65+
"module_name": "agentops.instrumentation.google_genai",
66+
"class_name": "GoogleGenAIInstrumentor",
67+
"min_version": "0.1.0",
68+
"package_name": "google-genai", # Actual pip package name
69+
},
70+
}
71+
72+
# Configuration for utility instrumentors
73+
UTILITY_INSTRUMENTORS: dict[str, InstrumentorConfig] = {
74+
"concurrent.futures": {
75+
"module_name": "agentops.instrumentation.concurrent_futures",
76+
"class_name": "ConcurrentFuturesInstrumentor",
77+
"min_version": "3.7.0", # Python 3.7+ (concurrent.futures is stdlib)
78+
"package_name": "python", # Special case for stdlib modules
79+
},
80+
}
81+
82+
# Configuration for supported agentic libraries
83+
AGENTIC_LIBRARIES: dict[str, InstrumentorConfig] = {
84+
"crewai": {
85+
"module_name": "agentops.instrumentation.crewai",
86+
"class_name": "CrewAIInstrumentor",
87+
"min_version": "0.56.0",
88+
},
89+
"autogen": {"module_name": "agentops.instrumentation.ag2", "class_name": "AG2Instrumentor", "min_version": "0.1.0"},
90+
"agents": {
91+
"module_name": "agentops.instrumentation.openai_agents",
92+
"class_name": "OpenAIAgentsInstrumentor",
93+
"min_version": "0.0.1",
94+
},
95+
"google.adk": {
96+
"module_name": "agentops.instrumentation.google_adk",
97+
"class_name": "GoogleADKInstrumentor",
98+
"min_version": "0.1.0",
99+
},
100+
}
101+
102+
# Combine all target packages for monitoring
103+
TARGET_PACKAGES = set(PROVIDERS.keys()) | set(AGENTIC_LIBRARIES.keys()) | set(UTILITY_INSTRUMENTORS.keys())
104+
105+
# Create a single instance of the manager
106+
# _manager = InstrumentationManager() # Removed
107+
38108
# Module-level state variables
39109
_active_instrumentors: list[BaseInstrumentor] = []
40110
_original_builtins_import = builtins.__import__ # Store original import
@@ -49,6 +119,16 @@ def _is_installed_package(module_obj: ModuleType, package_name_key: str) -> bool
49119
rather than a local module, especially when names might collide.
50120
`package_name_key` is the key from TARGET_PACKAGES (e.g., 'agents', 'google.adk').
51121
"""
122+
# Special case for stdlib modules (marked with package_name="python" in UTILITY_INSTRUMENTORS)
123+
if (
124+
package_name_key in UTILITY_INSTRUMENTORS
125+
and UTILITY_INSTRUMENTORS[package_name_key].get("package_name") == "python"
126+
):
127+
logger.debug(
128+
f"_is_installed_package: Module '{package_name_key}' is a Python standard library module. Considering it an installed package."
129+
)
130+
return True
131+
52132
if not hasattr(module_obj, "__file__") or not module_obj.__file__:
53133
logger.debug(
54134
f"_is_installed_package: Module '{package_name_key}' has no __file__, assuming it might be an SDK namespace package. Returning True."
@@ -141,7 +221,7 @@ def _uninstrument_providers():
141221
def _should_instrument_package(package_name: str) -> bool:
142222
"""
143223
Determine if a package should be instrumented based on current state.
144-
Handles special cases for agentic libraries and providers.
224+
Handles special cases for agentic libraries, providers, and utility instrumentors.
145225
"""
146226
global _has_agentic_library
147227

@@ -150,6 +230,12 @@ def _should_instrument_package(package_name: str) -> bool:
150230
logger.debug(f"_should_instrument_package: '{package_name}' already instrumented by AgentOps. Skipping.")
151231
return False
152232

233+
# Utility instrumentors should always be instrumented regardless of agentic library state
234+
if package_name in UTILITY_INSTRUMENTORS:
235+
logger.debug(f"_should_instrument_package: '{package_name}' is a utility instrumentor. Always allowing.")
236+
return True
237+
238+
# Only apply agentic/provider logic if it's NOT a utility instrumentor
153239
is_target_agentic = package_name in AGENTIC_LIBRARIES
154240
is_target_provider = package_name in PROVIDERS
155241

@@ -198,14 +284,18 @@ def _perform_instrumentation(package_name: str):
198284
return
199285

200286
# Get the appropriate configuration for the package
201-
# Ensure package_name is a key in either PROVIDERS or AGENTIC_LIBRARIES
202-
if package_name not in PROVIDERS and package_name not in AGENTIC_LIBRARIES:
287+
# Ensure package_name is a key in either PROVIDERS, AGENTIC_LIBRARIES, or UTILITY_INSTRUMENTORS
288+
if (
289+
package_name not in PROVIDERS
290+
and package_name not in AGENTIC_LIBRARIES
291+
and package_name not in UTILITY_INSTRUMENTORS
292+
):
203293
logger.debug(
204-
f"_perform_instrumentation: Package '{package_name}' not found in PROVIDERS or AGENTIC_LIBRARIES. Skipping."
294+
f"_perform_instrumentation: Package '{package_name}' not found in PROVIDERS, AGENTIC_LIBRARIES, or UTILITY_INSTRUMENTORS. Skipping."
205295
)
206296
return
207297

208-
config = PROVIDERS.get(package_name) or AGENTIC_LIBRARIES[package_name]
298+
config = PROVIDERS.get(package_name) or AGENTIC_LIBRARIES.get(package_name) or UTILITY_INSTRUMENTORS[package_name]
209299
loader = InstrumentorLoader(**config)
210300

211301
# instrument_one already checks loader.should_activate
@@ -327,74 +417,6 @@ def _import_monitor(name: str, globals_dict=None, locals_dict=None, fromlist=(),
327417
return module
328418

329419

330-
# Define the structure for instrumentor configurations
331-
class InstrumentorConfig(TypedDict):
332-
module_name: str
333-
class_name: str
334-
min_version: str
335-
package_name: NotRequired[str] # Optional: actual pip package name if different from module
336-
337-
338-
# Configuration for supported LLM providers
339-
PROVIDERS: dict[str, InstrumentorConfig] = {
340-
"openai": {
341-
"module_name": "agentops.instrumentation.openai",
342-
"class_name": "OpenAIInstrumentor",
343-
"min_version": "1.0.0",
344-
"package_name": "openai", # Actual pip package name
345-
},
346-
"anthropic": {
347-
"module_name": "agentops.instrumentation.anthropic",
348-
"class_name": "AnthropicInstrumentor",
349-
"min_version": "0.32.0",
350-
"package_name": "anthropic", # Actual pip package name
351-
},
352-
"ibm_watsonx_ai": {
353-
"module_name": "agentops.instrumentation.ibm_watsonx_ai",
354-
"class_name": "IBMWatsonXInstrumentor",
355-
"min_version": "0.1.0",
356-
"package_name": "ibm-watsonx-ai", # Actual pip package name
357-
},
358-
"google.genai": {
359-
"module_name": "agentops.instrumentation.google_genai",
360-
"class_name": "GoogleGenAIInstrumentor",
361-
"min_version": "0.1.0",
362-
"package_name": "google-genai", # Actual pip package name
363-
},
364-
}
365-
366-
# Configuration for supported agentic libraries
367-
AGENTIC_LIBRARIES: dict[str, InstrumentorConfig] = {
368-
"crewai": {
369-
"module_name": "agentops.instrumentation.crewai",
370-
"class_name": "CrewAIInstrumentor",
371-
"min_version": "0.56.0",
372-
"package_name": "crewai", # Actual pip package name
373-
},
374-
"autogen": {"module_name": "agentops.instrumentation.ag2", "class_name": "AG2Instrumentor", "min_version": "0.1.0"},
375-
"agents": {
376-
"module_name": "agentops.instrumentation.openai_agents",
377-
"class_name": "OpenAIAgentsInstrumentor",
378-
"min_version": "0.0.1",
379-
"package_name": "openai-agents",
380-
},
381-
"google.adk": {
382-
"module_name": "agentops.instrumentation.google_adk",
383-
"class_name": "GoogleADKInstrumentor",
384-
"min_version": "0.1.0",
385-
"package_name": "google-adk", # Actual pip package name
386-
},
387-
"agno": {
388-
"module_name": "agentops.instrumentation.agno",
389-
"class_name": "AgnoInstrumentor",
390-
"min_version": "0.1.0",
391-
},
392-
}
393-
394-
# Combine all target packages for monitoring
395-
TARGET_PACKAGES = set(PROVIDERS.keys()) | set(AGENTIC_LIBRARIES.keys())
396-
397-
398420
@dataclass
399421
class InstrumentorLoader:
400422
"""
@@ -416,6 +438,13 @@ def module(self) -> ModuleType:
416438
def should_activate(self) -> bool:
417439
"""Check if the package is available and meets version requirements."""
418440
try:
441+
# Special case for stdlib modules (like concurrent.futures)
442+
if self.package_name == "python":
443+
import sys
444+
445+
python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
446+
return Version(python_version) >= parse(self.min_version)
447+
419448
# Use explicit package_name if provided, otherwise derive from module_name
420449
if self.package_name:
421450
provider_name = self.package_name
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""
2+
Instrumentation for concurrent.futures module.
3+
4+
This module provides automatic instrumentation for ThreadPoolExecutor to ensure
5+
proper OpenTelemetry context propagation across thread boundaries.
6+
"""
7+
8+
from .instrumentation import ConcurrentFuturesInstrumentor
9+
10+
__all__ = ["ConcurrentFuturesInstrumentor"]

0 commit comments

Comments
 (0)