From fda9cd1cae2a34d1e3094d5434165188b91bd042 Mon Sep 17 00:00:00 2001 From: sipercai <53717475+sipercai@users.noreply.github.com> Date: Tue, 30 Jun 2026 12:24:08 +0800 Subject: [PATCH 01/21] fix: split bootstrap registry fragments --- .github/scripts/detect_loongsuite_changes.py | 37 ++ .../tests/test_detect_loongsuite_changes.py | 60 +++ .../tests/test_error_scenarios.py | 18 +- .../src/loongsuite/distro/bootstrap_gen.py | 326 +------------- .../distro/bootstrap_registry/__init__.py | 82 ++++ .../loongsuite_instrumentation_agentscope.py | 30 ++ .../loongsuite_instrumentation_agno.py | 30 ++ .../loongsuite_instrumentation_algotune.py | 28 ++ .../loongsuite_instrumentation_autogen.py | 30 ++ .../loongsuite_instrumentation_bfclv4.py | 30 ++ ...gsuite_instrumentation_claude_agent_sdk.py | 30 ++ .../loongsuite_instrumentation_claw_eval.py | 30 ++ .../loongsuite_instrumentation_crewai.py | 30 ++ .../loongsuite_instrumentation_dashscope.py | 30 ++ .../loongsuite_instrumentation_deepagents.py | 30 ++ .../loongsuite_instrumentation_dify.py | 28 ++ .../loongsuite_instrumentation_google_adk.py | 30 ++ ...loongsuite_instrumentation_hermes_agent.py | 30 ++ .../loongsuite_instrumentation_langchain.py | 30 ++ .../loongsuite_instrumentation_langgraph.py | 30 ++ .../loongsuite_instrumentation_litellm.py | 30 ++ .../loongsuite_instrumentation_mcp.py | 30 ++ .../loongsuite_instrumentation_mem0.py | 30 ++ ...loongsuite_instrumentation_minisweagent.py | 30 ++ .../loongsuite_instrumentation_openai_v2.py | 30 ++ .../loongsuite_instrumentation_openhands.py | 28 ++ .../loongsuite_instrumentation_qwen_agent.py | 30 ++ .../loongsuite_instrumentation_qwenpaw.py | 31 ++ .../loongsuite_instrumentation_slop_code.py | 30 ++ .../loongsuite_instrumentation_terminus2.py | 30 ++ .../loongsuite_instrumentation_vertexai.py | 30 ++ .../loongsuite_instrumentation_vita.py | 30 ++ .../loongsuite_instrumentation_webarena.py | 30 ++ .../loongsuite_instrumentation_widesearch.py | 30 ++ .../loongsuite_instrumentation_wildtool.py | 30 ++ .../opentelemetry_instrumentation_aio_pika.py | 30 ++ ...elemetry_instrumentation_aiohttp_client.py | 30 ++ ...elemetry_instrumentation_aiohttp_server.py | 30 ++ .../opentelemetry_instrumentation_aiokafka.py | 30 ++ .../opentelemetry_instrumentation_aiopg.py | 30 ++ .../opentelemetry_instrumentation_asgi.py | 30 ++ ...pentelemetry_instrumentation_asyncclick.py | 30 ++ .../opentelemetry_instrumentation_asyncio.py | 28 ++ .../opentelemetry_instrumentation_asyncpg.py | 30 ++ .../opentelemetry_instrumentation_boto.py | 30 ++ .../opentelemetry_instrumentation_boto3sqs.py | 30 ++ .../opentelemetry_instrumentation_botocore.py | 30 ++ ...opentelemetry_instrumentation_cassandra.py | 31 ++ .../opentelemetry_instrumentation_celery.py | 30 ++ .../opentelemetry_instrumentation_click.py | 30 ++ ...lemetry_instrumentation_confluent_kafka.py | 30 ++ .../opentelemetry_instrumentation_dbapi.py | 28 ++ .../opentelemetry_instrumentation_django.py | 30 ++ ...telemetry_instrumentation_elasticsearch.py | 30 ++ .../opentelemetry_instrumentation_falcon.py | 30 ++ .../opentelemetry_instrumentation_fastapi.py | 30 ++ .../opentelemetry_instrumentation_flask.py | 30 ++ .../opentelemetry_instrumentation_grpc.py | 30 ++ .../opentelemetry_instrumentation_httpx.py | 30 ++ .../opentelemetry_instrumentation_jinja2.py | 30 ++ ...ntelemetry_instrumentation_kafka_python.py | 31 ++ .../opentelemetry_instrumentation_logging.py | 28 ++ .../opentelemetry_instrumentation_mysql.py | 30 ++ ...entelemetry_instrumentation_mysqlclient.py | 30 ++ .../opentelemetry_instrumentation_pika.py | 30 ++ .../opentelemetry_instrumentation_psycopg.py | 30 ++ .../opentelemetry_instrumentation_psycopg2.py | 31 ++ ...pentelemetry_instrumentation_pymemcache.py | 30 ++ .../opentelemetry_instrumentation_pymongo.py | 30 ++ .../opentelemetry_instrumentation_pymssql.py | 30 ++ .../opentelemetry_instrumentation_pymysql.py | 30 ++ .../opentelemetry_instrumentation_pyramid.py | 30 ++ .../opentelemetry_instrumentation_redis.py | 30 ++ ...opentelemetry_instrumentation_remoulade.py | 30 ++ .../opentelemetry_instrumentation_requests.py | 30 ++ ...pentelemetry_instrumentation_sqlalchemy.py | 30 ++ .../opentelemetry_instrumentation_sqlite3.py | 28 ++ ...opentelemetry_instrumentation_starlette.py | 30 ++ ...elemetry_instrumentation_system_metrics.py | 30 ++ ...opentelemetry_instrumentation_threading.py | 28 ++ .../opentelemetry_instrumentation_tornado.py | 30 ++ ...entelemetry_instrumentation_tortoiseorm.py | 31 ++ .../opentelemetry_instrumentation_urllib.py | 28 ++ .../opentelemetry_instrumentation_urllib3.py | 30 ++ .../opentelemetry_instrumentation_wsgi.py | 28 ++ .../generate_loongsuite_bootstrap.py | 397 ++++++++++++------ .../test_generate_loongsuite_bootstrap.py | 172 ++++++++ tox-loongsuite.ini | 3 +- 88 files changed, 3012 insertions(+), 468 deletions(-) create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/__init__.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_agentscope.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_agno.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_algotune.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_autogen.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_bfclv4.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_claude_agent_sdk.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_claw_eval.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_crewai.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_dashscope.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_deepagents.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_dify.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_google_adk.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_hermes_agent.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_langchain.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_langgraph.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_litellm.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_mcp.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_mem0.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_minisweagent.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_openai_v2.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_openhands.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_qwen_agent.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_qwenpaw.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_slop_code.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_terminus2.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_vertexai.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_vita.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_webarena.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_widesearch.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_wildtool.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_aio_pika.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_aiohttp_client.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_aiohttp_server.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_aiokafka.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_aiopg.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_asgi.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_asyncclick.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_asyncio.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_asyncpg.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_boto.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_boto3sqs.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_botocore.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_cassandra.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_celery.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_click.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_confluent_kafka.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_dbapi.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_django.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_elasticsearch.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_falcon.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_fastapi.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_flask.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_grpc.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_httpx.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_jinja2.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_kafka_python.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_logging.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_mysql.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_mysqlclient.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_pika.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_psycopg.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_psycopg2.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_pymemcache.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_pymongo.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_pymssql.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_pymysql.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_pyramid.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_redis.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_remoulade.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_requests.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_sqlalchemy.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_sqlite3.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_starlette.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_system_metrics.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_threading.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_tornado.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_tortoiseorm.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_urllib.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_urllib3.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_wsgi.py create mode 100644 scripts/loongsuite/tests/test_generate_loongsuite_bootstrap.py diff --git a/.github/scripts/detect_loongsuite_changes.py b/.github/scripts/detect_loongsuite_changes.py index dd3b39541..9ad7c1c85 100644 --- a/.github/scripts/detect_loongsuite_changes.py +++ b/.github/scripts/detect_loongsuite_changes.py @@ -45,6 +45,9 @@ TOX_LOONGSUITE_INI_PATH = "tox-loongsuite.ini" UTIL_GENAI_PREFIX = "util/opentelemetry-util-genai/" LOONGSUITE_INSTRUMENTATION_PREFIX = "instrumentation-loongsuite/" +BOOTSTRAP_REGISTRY_PREFIX = ( + "loongsuite-distro/src/loongsuite/distro/bootstrap_registry/" +) DOC_ONLY_SUFFIXES = (".md", ".rst") REPO_ROOT = Path.cwd() TOX_LOONGSUITE_INI = REPO_ROOT / TOX_LOONGSUITE_INI_PATH @@ -150,6 +153,30 @@ def _loongsuite_package_from_path(path: str) -> str | None: return None +def _loongsuite_package_from_bootstrap_registry_path(path: str) -> str | None: + normalized = path.strip("/") + if not normalized.startswith(BOOTSTRAP_REGISTRY_PREFIX): + return None + + relative_path = normalized.removeprefix(BOOTSTRAP_REGISTRY_PREFIX) + if "/" in relative_path: + return None + + registry_path = PurePosixPath(relative_path) + if registry_path.suffix != ".py": + return None + + module_name = registry_path.stem + if module_name == "__init__" or module_name.startswith("_"): + return None + + package = module_name.replace("_", "-") + if package.startswith("loongsuite-instrumentation-"): + return package + + return None + + def _known_loongsuite_packages() -> set[str]: text = TOX_LOONGSUITE_INI.read_text(encoding="utf-8") packages = set() @@ -339,6 +366,16 @@ def _detect_outputs() -> dict[str, str]: tox_changed = True continue + registry_package = _loongsuite_package_from_bootstrap_registry_path( + changed_file + ) + if registry_package: + if registry_package not in known_packages: + unknown_packages.add(registry_package) + else: + packages.add(registry_package) + continue + if _requires_full_run(changed_file): return _full_outputs( f"shared LoongSuite file changed: {changed_file}" diff --git a/.github/scripts/tests/test_detect_loongsuite_changes.py b/.github/scripts/tests/test_detect_loongsuite_changes.py index f81c3b4d3..ec031fc65 100644 --- a/.github/scripts/tests/test_detect_loongsuite_changes.py +++ b/.github/scripts/tests/test_detect_loongsuite_changes.py @@ -210,6 +210,66 @@ def test_generated_workflow_only_change_skips_jobs(monkeypatch, tmp_path): assert outputs["packages"] == "||" +def test_bootstrap_registry_fragment_scopes_package_change( + monkeypatch, + tmp_path, +): + outputs = _run_detector( + monkeypatch, + tmp_path, + [ + "loongsuite-distro/src/loongsuite/distro/bootstrap_registry/" + "loongsuite_instrumentation_crewai.py" + ], + known_packages={"loongsuite-instrumentation-crewai"}, + ) + + assert outputs["full"] == "false" + assert outputs["packages"] == "|loongsuite-instrumentation-crewai|" + + +def test_bootstrap_registry_loader_change_runs_full_suite( + monkeypatch, + tmp_path, +): + outputs = _run_detector( + monkeypatch, + tmp_path, + [ + "loongsuite-distro/src/loongsuite/distro/bootstrap_registry/" + "__init__.py" + ], + ) + + assert outputs["full"] == "true" + assert outputs["reason"] == ( + "shared LoongSuite file changed: " + "loongsuite-distro/src/loongsuite/distro/bootstrap_registry/" + "__init__.py" + ) + + +def test_upstream_bootstrap_registry_fragment_runs_full_suite( + monkeypatch, + tmp_path, +): + outputs = _run_detector( + monkeypatch, + tmp_path, + [ + "loongsuite-distro/src/loongsuite/distro/bootstrap_registry/" + "opentelemetry_instrumentation_aiohttp_client.py" + ], + ) + + assert outputs["full"] == "true" + assert outputs["reason"] == ( + "shared LoongSuite file changed: " + "loongsuite-distro/src/loongsuite/distro/bootstrap_registry/" + "opentelemetry_instrumentation_aiohttp_client.py" + ) + + def test_release_workflow_change_runs_full_suite(monkeypatch, tmp_path): outputs = _run_detector( monkeypatch, diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-crewai/tests/test_error_scenarios.py b/instrumentation-loongsuite/loongsuite-instrumentation-crewai/tests/test_error_scenarios.py index 7cca92472..1dfab1f99 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-crewai/tests/test_error_scenarios.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-crewai/tests/test_error_scenarios.py @@ -103,11 +103,11 @@ def tearDown(self): def test_api_key_missing(self): """ - Test execution with missing API key. + Test execution with authentication failure. Business Demo: - Creates a Crew with 1 Agent - - API key is empty/missing + - API key is rejected by the LLM service - Executes 1 Task that fails due to authentication Verification: @@ -115,11 +115,11 @@ def test_api_key_missing(self): - Span records authentication exception in events - Input messages are still captured for context """ - # Temporarily remove API keys + # Use non-empty fake keys so provider validation reaches kickoff. original_dashscope_key = os.environ.get("DASHSCOPE_API_KEY") original_openai_key = os.environ.get("OPENAI_API_KEY") - os.environ["DASHSCOPE_API_KEY"] = "" - os.environ["OPENAI_API_KEY"] = "" + os.environ["DASHSCOPE_API_KEY"] = "fake-key" + os.environ["OPENAI_API_KEY"] = "fake-key" try: agent = Agent( @@ -150,9 +150,13 @@ def test_api_key_missing(self): finally: # Restore API keys - if original_dashscope_key: + if original_dashscope_key is None: + os.environ.pop("DASHSCOPE_API_KEY", None) + else: os.environ["DASHSCOPE_API_KEY"] = original_dashscope_key - if original_openai_key: + if original_openai_key is None: + os.environ.pop("OPENAI_API_KEY", None) + else: os.environ["OPENAI_API_KEY"] = original_openai_key # Verify spans diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_gen.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_gen.py index 474f03cee..d297ba2a7 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_gen.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_gen.py @@ -12,325 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. -# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. -# -# Generated with options: -# --upstream-version: (from source) -# --loongsuite-version: (from source) +"""Compatibility exports for the LoongSuite bootstrap registry.""" -libraries = [ - { - "library": "openai >= 1.26.0", - "instrumentation": "loongsuite-instrumentation-openai-v2", - }, - { - "library": "google-cloud-aiplatform >= 1.64", - "instrumentation": "loongsuite-instrumentation-vertexai>=2.0b0", - }, - { - "library": "aio_pika >= 7.2.0, < 10.0.0", - "instrumentation": "opentelemetry-instrumentation-aio-pika==0.62b0.dev", - }, - { - "library": "aiohttp ~= 3.0", - "instrumentation": "opentelemetry-instrumentation-aiohttp-client==0.62b0.dev", - }, - { - "library": "aiohttp ~= 3.0", - "instrumentation": "opentelemetry-instrumentation-aiohttp-server==0.62b0.dev", - }, - { - "library": "aiokafka >= 0.8, < 1.0", - "instrumentation": "opentelemetry-instrumentation-aiokafka==0.62b0.dev", - }, - { - "library": "aiopg >= 0.13.0, < 2.0.0", - "instrumentation": "opentelemetry-instrumentation-aiopg==0.62b0.dev", - }, - { - "library": "asgiref ~= 3.0", - "instrumentation": "opentelemetry-instrumentation-asgi==0.62b0.dev", - }, - { - "library": "asyncclick ~= 8.0", - "instrumentation": "opentelemetry-instrumentation-asyncclick==0.62b0.dev", - }, - { - "library": "asyncpg >= 0.12.0", - "instrumentation": "opentelemetry-instrumentation-asyncpg==0.62b0.dev", - }, - { - "library": "boto~=2.0", - "instrumentation": "opentelemetry-instrumentation-boto==0.62b0.dev", - }, - { - "library": "boto3 ~= 1.0", - "instrumentation": "opentelemetry-instrumentation-boto3sqs==0.62b0.dev", - }, - { - "library": "botocore ~= 1.0", - "instrumentation": "opentelemetry-instrumentation-botocore==0.62b0.dev", - }, - { - "library": "cassandra-driver ~= 3.25", - "instrumentation": "opentelemetry-instrumentation-cassandra==0.62b0.dev", - }, - { - "library": "scylla-driver ~= 3.25", - "instrumentation": "opentelemetry-instrumentation-cassandra==0.62b0.dev", - }, - { - "library": "celery >= 4.0, < 6.0", - "instrumentation": "opentelemetry-instrumentation-celery==0.62b0.dev", - }, - { - "library": "click >= 8.1.3, < 9.0.0", - "instrumentation": "opentelemetry-instrumentation-click==0.62b0.dev", - }, - { - "library": "confluent-kafka >= 1.8.2, <= 2.13.0", - "instrumentation": "opentelemetry-instrumentation-confluent-kafka==0.62b0.dev", - }, - { - "library": "django >= 2.0", - "instrumentation": "opentelemetry-instrumentation-django==0.62b0.dev", - }, - { - "library": "elasticsearch >= 6.0", - "instrumentation": "opentelemetry-instrumentation-elasticsearch==0.62b0.dev", - }, - { - "library": "falcon >= 1.4.1, < 5.0.0", - "instrumentation": "opentelemetry-instrumentation-falcon==0.62b0.dev", - }, - { - "library": "fastapi ~= 0.92", - "instrumentation": "opentelemetry-instrumentation-fastapi==0.62b0.dev", - }, - { - "library": "flask >= 1.0", - "instrumentation": "opentelemetry-instrumentation-flask==0.62b0.dev", - }, - { - "library": "grpcio >= 1.42.0", - "instrumentation": "opentelemetry-instrumentation-grpc==0.62b0.dev", - }, - { - "library": "httpx >= 0.18.0", - "instrumentation": "opentelemetry-instrumentation-httpx==0.62b0.dev", - }, - { - "library": "jinja2 >= 2.7, < 4.0", - "instrumentation": "opentelemetry-instrumentation-jinja2==0.62b0.dev", - }, - { - "library": "kafka-python >= 2.0, < 3.0", - "instrumentation": "opentelemetry-instrumentation-kafka-python==0.62b0.dev", - }, - { - "library": "kafka-python-ng >= 2.0, < 3.0", - "instrumentation": "opentelemetry-instrumentation-kafka-python==0.62b0.dev", - }, - { - "library": "mysql-connector-python >= 8.0, < 10.0", - "instrumentation": "opentelemetry-instrumentation-mysql==0.62b0.dev", - }, - { - "library": "mysqlclient < 3", - "instrumentation": "opentelemetry-instrumentation-mysqlclient==0.62b0.dev", - }, - { - "library": "pika >= 0.12.0", - "instrumentation": "opentelemetry-instrumentation-pika==0.62b0.dev", - }, - { - "library": "psycopg >= 3.1.0", - "instrumentation": "opentelemetry-instrumentation-psycopg==0.62b0.dev", - }, - { - "library": "psycopg2 >= 2.7.3.1", - "instrumentation": "opentelemetry-instrumentation-psycopg2==0.62b0.dev", - }, - { - "library": "psycopg2-binary >= 2.7.3.1", - "instrumentation": "opentelemetry-instrumentation-psycopg2==0.62b0.dev", - }, - { - "library": "pymemcache >= 1.3.5, < 5", - "instrumentation": "opentelemetry-instrumentation-pymemcache==0.62b0.dev", - }, - { - "library": "pymongo >= 3.1, < 5.0", - "instrumentation": "opentelemetry-instrumentation-pymongo==0.62b0.dev", - }, - { - "library": "pymssql >= 2.1.5, < 3", - "instrumentation": "opentelemetry-instrumentation-pymssql==0.62b0.dev", - }, - { - "library": "PyMySQL < 2", - "instrumentation": "opentelemetry-instrumentation-pymysql==0.62b0.dev", - }, - { - "library": "pyramid >= 1.7", - "instrumentation": "opentelemetry-instrumentation-pyramid==0.62b0.dev", - }, - { - "library": "redis >= 2.6", - "instrumentation": "opentelemetry-instrumentation-redis==0.62b0.dev", - }, - { - "library": "remoulade >= 0.50", - "instrumentation": "opentelemetry-instrumentation-remoulade==0.62b0.dev", - }, - { - "library": "requests ~= 2.0", - "instrumentation": "opentelemetry-instrumentation-requests==0.62b0.dev", - }, - { - "library": "sqlalchemy >= 1.0.0, < 2.1.0", - "instrumentation": "opentelemetry-instrumentation-sqlalchemy==0.62b0.dev", - }, - { - "library": "starlette >= 0.13", - "instrumentation": "opentelemetry-instrumentation-starlette==0.62b0.dev", - }, - { - "library": "psutil >= 5", - "instrumentation": "opentelemetry-instrumentation-system-metrics==0.62b0.dev", - }, - { - "library": "tornado >= 5.1.1", - "instrumentation": "opentelemetry-instrumentation-tornado==0.62b0.dev", - }, - { - "library": "tortoise-orm >= 0.17.0", - "instrumentation": "opentelemetry-instrumentation-tortoiseorm==0.62b0.dev", - }, - { - "library": "pydantic >= 1.10.2", - "instrumentation": "opentelemetry-instrumentation-tortoiseorm==0.62b0.dev", - }, - { - "library": "urllib3 >= 1.0.0, < 3.0.0", - "instrumentation": "opentelemetry-instrumentation-urllib3==0.62b0.dev", - }, - { - "library": "agentscope >= 1.0.0", - "instrumentation": "loongsuite-instrumentation-agentscope==0.7.0.dev", - }, - { - "library": "agno >= 2.0.0, < 3", - "instrumentation": "loongsuite-instrumentation-agno==0.7.0.dev", - }, - { - "library": "autogen-agentchat >= 0.7.0, < 0.8.0", - "instrumentation": "loongsuite-instrumentation-autogen==0.7.0.dev", - }, - { - "library": "bfcl-eval >= 4.0.0", - "instrumentation": "loongsuite-instrumentation-bfclv4==0.7.0.dev", - }, - { - "library": "claude-agent-sdk >= 0.1.0", - "instrumentation": "loongsuite-instrumentation-claude-agent-sdk==0.7.0.dev", - }, - { - "library": "claw-eval >= 0.1.0", - "instrumentation": "loongsuite-instrumentation-claw-eval==0.7.0.dev", - }, - { - "library": "crewai >= 0.80.0", - "instrumentation": "loongsuite-instrumentation-crewai==0.7.0.dev", - }, - { - "library": "dashscope >= 1.0.0", - "instrumentation": "loongsuite-instrumentation-dashscope==0.7.0.dev", - }, - { - "library": "deepagents >= 0.6.0, < 0.7.0", - "instrumentation": "loongsuite-instrumentation-deepagents==0.7.0.dev", - }, - { - "library": "google-adk >= 0.1.0", - "instrumentation": "loongsuite-instrumentation-google-adk==0.7.0.dev", - }, - { - "library": "openai >= 1.0.0", - "instrumentation": "loongsuite-instrumentation-hermes-agent==0.7.0.dev", - }, - { - "library": "langchain_core >= 0.1.0", - "instrumentation": "loongsuite-instrumentation-langchain==0.7.0.dev", - }, - { - "library": "langgraph >= 0.2", - "instrumentation": "loongsuite-instrumentation-langgraph==0.7.0.dev", - }, - { - "library": "litellm >= 1.0.0", - "instrumentation": "loongsuite-instrumentation-litellm==0.7.0.dev", - }, - { - "library": "mcp >= 1.3.0, <= 1.25.0", - "instrumentation": "loongsuite-instrumentation-mcp==0.7.0.dev", - }, - { - "library": "mem0ai >= 1.0.0, < 2.0.0", - "instrumentation": "loongsuite-instrumentation-mem0==0.7.0.dev", - }, - { - "library": "mini-swe-agent >= 2.2.0", - "instrumentation": "loongsuite-instrumentation-minisweagent==0.7.0.dev", - }, - { - "library": "qwen-agent >= 0.0.20", - "instrumentation": "loongsuite-instrumentation-qwen-agent==0.7.0.dev", - }, - { - "library": "qwenpaw >= 1.1.0", - "instrumentation": "loongsuite-instrumentation-qwenpaw==0.7.0.dev", - }, - { - "library": "copaw >= 0.1.0, <= 1.0.2", - "instrumentation": "loongsuite-instrumentation-qwenpaw==0.7.0.dev", - }, - { - "library": "slop-code-bench >= 0.1", - "instrumentation": "loongsuite-instrumentation-slop-code==0.7.0.dev", - }, - { - "library": "terminal-bench >= 0.1.0", - "instrumentation": "loongsuite-instrumentation-terminus2==0.7.0.dev", - }, - { - "library": "vita >= 0.0.1", - "instrumentation": "loongsuite-instrumentation-vita==0.7.0.dev", - }, - { - "library": "webarena >= 0.0.1", - "instrumentation": "loongsuite-instrumentation-webarena==0.7.0.dev", - }, - { - "library": "widesearch >= 0.1.0", - "instrumentation": "loongsuite-instrumentation-widesearch==0.7.0.dev", - }, - { - "library": "openai >= 1.0.0", - "instrumentation": "loongsuite-instrumentation-wildtool==0.7.0.dev", - }, -] +from loongsuite.distro.bootstrap_registry import ( + default_instrumentations, + libraries, +) -default_instrumentations = [ - "opentelemetry-instrumentation-asyncio==0.62b0.dev", - "opentelemetry-instrumentation-dbapi==0.62b0.dev", - "opentelemetry-instrumentation-logging==0.62b0.dev", - "opentelemetry-instrumentation-sqlite3==0.62b0.dev", - "opentelemetry-instrumentation-threading==0.62b0.dev", - "opentelemetry-instrumentation-urllib==0.62b0.dev", - "opentelemetry-instrumentation-wsgi==0.62b0.dev", - "loongsuite-instrumentation-algotune==0.7.0.dev", - "loongsuite-instrumentation-dify==0.7.0.dev", - "loongsuite-instrumentation-openhands==0.7.0.dev", -] +__all__ = ["default_instrumentations", "libraries"] diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/__init__.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/__init__.py new file mode 100644 index 000000000..b14dd617a --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/__init__.py @@ -0,0 +1,82 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Package-scoped bootstrap registry fragments for LoongSuite distro.""" + +from __future__ import annotations + +from importlib import import_module +from pkgutil import iter_modules +from typing import Any + +_SOURCE_SORT_ORDER = { + "genai-renamed": 0, + "upstream": 1, + "loongsuite": 2, +} + + +def _iter_registry_entries() -> list[dict[str, Any]]: + entries: list[dict[str, Any]] = [] + package_path = globals()["__path__"] + for module_info in sorted( + iter_modules(package_path), + key=lambda info: info.name, + ): + if module_info.ispkg or module_info.name.startswith("_"): + continue + + module = import_module(f"{__name__}.{module_info.name}") + registry = getattr(module, "REGISTRY", None) + if registry is not None: + entries.append(registry) + + return sorted( + entries, + key=lambda registry: ( + _SOURCE_SORT_ORDER.get(registry.get("source"), 99), + registry.get("package", ""), + ), + ) + + +def load_bootstrap_registry() -> tuple[list[dict[str, str]], list[str]]: + libraries: list[dict[str, str]] = [] + default_instrumentations: list[str] = [] + + for registry in _iter_registry_entries(): + instrumentation = registry["instrumentation"] + target_libraries = registry.get("libraries") or [] + if not target_libraries: + default_instrumentations.append(instrumentation) + continue + + for target_library in target_libraries: + libraries.append( + { + "library": target_library, + "instrumentation": instrumentation, + } + ) + + return libraries, default_instrumentations + + +libraries, default_instrumentations = load_bootstrap_registry() + +__all__ = [ + "default_instrumentations", + "libraries", + "load_bootstrap_registry", +] diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_agentscope.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_agentscope.py new file mode 100644 index 000000000..67366a8e5 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_agentscope.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "loongsuite", + "package": "loongsuite-instrumentation-agentscope", + "instrumentation": "loongsuite-instrumentation-agentscope==0.7.0.dev", + "libraries": [ + "agentscope >= 1.0.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_agno.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_agno.py new file mode 100644 index 000000000..8900567ca --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_agno.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "loongsuite", + "package": "loongsuite-instrumentation-agno", + "instrumentation": "loongsuite-instrumentation-agno==0.7.0.dev", + "libraries": [ + "agno >= 2.0.0, < 3", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_algotune.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_algotune.py new file mode 100644 index 000000000..4d19cf800 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_algotune.py @@ -0,0 +1,28 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "loongsuite", + "package": "loongsuite-instrumentation-algotune", + "instrumentation": "loongsuite-instrumentation-algotune==0.7.0.dev", + "libraries": [], + "default": True, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_autogen.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_autogen.py new file mode 100644 index 000000000..fad1a340b --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_autogen.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "loongsuite", + "package": "loongsuite-instrumentation-autogen", + "instrumentation": "loongsuite-instrumentation-autogen==0.7.0.dev", + "libraries": [ + "autogen-agentchat >= 0.7.0, < 0.8.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_bfclv4.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_bfclv4.py new file mode 100644 index 000000000..6413bfa83 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_bfclv4.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "loongsuite", + "package": "loongsuite-instrumentation-bfclv4", + "instrumentation": "loongsuite-instrumentation-bfclv4==0.7.0.dev", + "libraries": [ + "bfcl-eval >= 4.0.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_claude_agent_sdk.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_claude_agent_sdk.py new file mode 100644 index 000000000..1e35bc2bc --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_claude_agent_sdk.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "loongsuite", + "package": "loongsuite-instrumentation-claude-agent-sdk", + "instrumentation": "loongsuite-instrumentation-claude-agent-sdk==0.7.0.dev", + "libraries": [ + "claude-agent-sdk >= 0.1.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_claw_eval.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_claw_eval.py new file mode 100644 index 000000000..9fa624b93 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_claw_eval.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "loongsuite", + "package": "loongsuite-instrumentation-claw-eval", + "instrumentation": "loongsuite-instrumentation-claw-eval==0.7.0.dev", + "libraries": [ + "claw-eval >= 0.1.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_crewai.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_crewai.py new file mode 100644 index 000000000..c0884657c --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_crewai.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "loongsuite", + "package": "loongsuite-instrumentation-crewai", + "instrumentation": "loongsuite-instrumentation-crewai==0.7.0.dev", + "libraries": [ + "crewai >= 0.80.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_dashscope.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_dashscope.py new file mode 100644 index 000000000..bd4b7dd04 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_dashscope.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "loongsuite", + "package": "loongsuite-instrumentation-dashscope", + "instrumentation": "loongsuite-instrumentation-dashscope==0.7.0.dev", + "libraries": [ + "dashscope >= 1.0.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_deepagents.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_deepagents.py new file mode 100644 index 000000000..b22695004 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_deepagents.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "loongsuite", + "package": "loongsuite-instrumentation-deepagents", + "instrumentation": "loongsuite-instrumentation-deepagents==0.7.0.dev", + "libraries": [ + "deepagents >= 0.6.0, < 0.7.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_dify.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_dify.py new file mode 100644 index 000000000..c62b18d1d --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_dify.py @@ -0,0 +1,28 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "loongsuite", + "package": "loongsuite-instrumentation-dify", + "instrumentation": "loongsuite-instrumentation-dify==0.7.0.dev", + "libraries": [], + "default": True, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_google_adk.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_google_adk.py new file mode 100644 index 000000000..eae6d6912 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_google_adk.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "loongsuite", + "package": "loongsuite-instrumentation-google-adk", + "instrumentation": "loongsuite-instrumentation-google-adk==0.7.0.dev", + "libraries": [ + "google-adk >= 0.1.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_hermes_agent.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_hermes_agent.py new file mode 100644 index 000000000..47406ffd8 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_hermes_agent.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "loongsuite", + "package": "loongsuite-instrumentation-hermes-agent", + "instrumentation": "loongsuite-instrumentation-hermes-agent==0.7.0.dev", + "libraries": [ + "openai >= 1.0.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_langchain.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_langchain.py new file mode 100644 index 000000000..d68d28959 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_langchain.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "loongsuite", + "package": "loongsuite-instrumentation-langchain", + "instrumentation": "loongsuite-instrumentation-langchain==0.7.0.dev", + "libraries": [ + "langchain_core >= 0.1.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_langgraph.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_langgraph.py new file mode 100644 index 000000000..2b52ff124 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_langgraph.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "loongsuite", + "package": "loongsuite-instrumentation-langgraph", + "instrumentation": "loongsuite-instrumentation-langgraph==0.7.0.dev", + "libraries": [ + "langgraph >= 0.2", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_litellm.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_litellm.py new file mode 100644 index 000000000..eeca638e5 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_litellm.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "loongsuite", + "package": "loongsuite-instrumentation-litellm", + "instrumentation": "loongsuite-instrumentation-litellm==0.7.0.dev", + "libraries": [ + "litellm >= 1.0.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_mcp.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_mcp.py new file mode 100644 index 000000000..a293ee1db --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_mcp.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "loongsuite", + "package": "loongsuite-instrumentation-mcp", + "instrumentation": "loongsuite-instrumentation-mcp==0.7.0.dev", + "libraries": [ + "mcp >= 1.3.0, <= 1.25.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_mem0.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_mem0.py new file mode 100644 index 000000000..f20839dd3 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_mem0.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "loongsuite", + "package": "loongsuite-instrumentation-mem0", + "instrumentation": "loongsuite-instrumentation-mem0==0.7.0.dev", + "libraries": [ + "mem0ai >= 1.0.0, < 2.0.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_minisweagent.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_minisweagent.py new file mode 100644 index 000000000..10349e1d3 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_minisweagent.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "loongsuite", + "package": "loongsuite-instrumentation-minisweagent", + "instrumentation": "loongsuite-instrumentation-minisweagent==0.7.0.dev", + "libraries": [ + "mini-swe-agent >= 2.2.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_openai_v2.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_openai_v2.py new file mode 100644 index 000000000..230c85c44 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_openai_v2.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "genai-renamed", + "package": "loongsuite-instrumentation-openai-v2", + "instrumentation": "loongsuite-instrumentation-openai-v2", + "libraries": [ + "openai >= 1.26.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_openhands.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_openhands.py new file mode 100644 index 000000000..adbaa570b --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_openhands.py @@ -0,0 +1,28 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "loongsuite", + "package": "loongsuite-instrumentation-openhands", + "instrumentation": "loongsuite-instrumentation-openhands==0.7.0.dev", + "libraries": [], + "default": True, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_qwen_agent.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_qwen_agent.py new file mode 100644 index 000000000..cb087d592 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_qwen_agent.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "loongsuite", + "package": "loongsuite-instrumentation-qwen-agent", + "instrumentation": "loongsuite-instrumentation-qwen-agent==0.7.0.dev", + "libraries": [ + "qwen-agent >= 0.0.20", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_qwenpaw.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_qwenpaw.py new file mode 100644 index 000000000..c244de352 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_qwenpaw.py @@ -0,0 +1,31 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "loongsuite", + "package": "loongsuite-instrumentation-qwenpaw", + "instrumentation": "loongsuite-instrumentation-qwenpaw==0.7.0.dev", + "libraries": [ + "qwenpaw >= 1.1.0", + "copaw >= 0.1.0, <= 1.0.2", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_slop_code.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_slop_code.py new file mode 100644 index 000000000..2084fe6fc --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_slop_code.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "loongsuite", + "package": "loongsuite-instrumentation-slop-code", + "instrumentation": "loongsuite-instrumentation-slop-code==0.7.0.dev", + "libraries": [ + "slop-code-bench >= 0.1", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_terminus2.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_terminus2.py new file mode 100644 index 000000000..09ce12c09 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_terminus2.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "loongsuite", + "package": "loongsuite-instrumentation-terminus2", + "instrumentation": "loongsuite-instrumentation-terminus2==0.7.0.dev", + "libraries": [ + "terminal-bench >= 0.1.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_vertexai.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_vertexai.py new file mode 100644 index 000000000..884b2421f --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_vertexai.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "genai-renamed", + "package": "loongsuite-instrumentation-vertexai", + "instrumentation": "loongsuite-instrumentation-vertexai>=2.0b0", + "libraries": [ + "google-cloud-aiplatform >= 1.64", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_vita.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_vita.py new file mode 100644 index 000000000..be49f87b2 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_vita.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "loongsuite", + "package": "loongsuite-instrumentation-vita", + "instrumentation": "loongsuite-instrumentation-vita==0.7.0.dev", + "libraries": [ + "vita >= 0.0.1", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_webarena.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_webarena.py new file mode 100644 index 000000000..dae9a5933 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_webarena.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "loongsuite", + "package": "loongsuite-instrumentation-webarena", + "instrumentation": "loongsuite-instrumentation-webarena==0.7.0.dev", + "libraries": [ + "webarena >= 0.0.1", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_widesearch.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_widesearch.py new file mode 100644 index 000000000..b1e846631 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_widesearch.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "loongsuite", + "package": "loongsuite-instrumentation-widesearch", + "instrumentation": "loongsuite-instrumentation-widesearch==0.7.0.dev", + "libraries": [ + "widesearch >= 0.1.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_wildtool.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_wildtool.py new file mode 100644 index 000000000..703fb6e2b --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_wildtool.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "loongsuite", + "package": "loongsuite-instrumentation-wildtool", + "instrumentation": "loongsuite-instrumentation-wildtool==0.7.0.dev", + "libraries": [ + "openai >= 1.0.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_aio_pika.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_aio_pika.py new file mode 100644 index 000000000..1d16cb8ec --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_aio_pika.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-aio-pika", + "instrumentation": "opentelemetry-instrumentation-aio-pika==0.62b0.dev", + "libraries": [ + "aio_pika >= 7.2.0, < 10.0.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_aiohttp_client.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_aiohttp_client.py new file mode 100644 index 000000000..acd024944 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_aiohttp_client.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-aiohttp-client", + "instrumentation": "opentelemetry-instrumentation-aiohttp-client==0.62b0.dev", + "libraries": [ + "aiohttp ~= 3.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_aiohttp_server.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_aiohttp_server.py new file mode 100644 index 000000000..1506e867f --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_aiohttp_server.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-aiohttp-server", + "instrumentation": "opentelemetry-instrumentation-aiohttp-server==0.62b0.dev", + "libraries": [ + "aiohttp ~= 3.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_aiokafka.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_aiokafka.py new file mode 100644 index 000000000..e2d816b41 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_aiokafka.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-aiokafka", + "instrumentation": "opentelemetry-instrumentation-aiokafka==0.62b0.dev", + "libraries": [ + "aiokafka >= 0.8, < 1.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_aiopg.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_aiopg.py new file mode 100644 index 000000000..8aeb42600 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_aiopg.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-aiopg", + "instrumentation": "opentelemetry-instrumentation-aiopg==0.62b0.dev", + "libraries": [ + "aiopg >= 0.13.0, < 2.0.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_asgi.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_asgi.py new file mode 100644 index 000000000..7c2e32a13 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_asgi.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-asgi", + "instrumentation": "opentelemetry-instrumentation-asgi==0.62b0.dev", + "libraries": [ + "asgiref ~= 3.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_asyncclick.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_asyncclick.py new file mode 100644 index 000000000..a97d1b2ac --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_asyncclick.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-asyncclick", + "instrumentation": "opentelemetry-instrumentation-asyncclick==0.62b0.dev", + "libraries": [ + "asyncclick ~= 8.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_asyncio.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_asyncio.py new file mode 100644 index 000000000..9d01d6296 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_asyncio.py @@ -0,0 +1,28 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-asyncio", + "instrumentation": "opentelemetry-instrumentation-asyncio==0.62b0.dev", + "libraries": [], + "default": True, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_asyncpg.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_asyncpg.py new file mode 100644 index 000000000..f1a9a32ce --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_asyncpg.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-asyncpg", + "instrumentation": "opentelemetry-instrumentation-asyncpg==0.62b0.dev", + "libraries": [ + "asyncpg >= 0.12.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_boto.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_boto.py new file mode 100644 index 000000000..e663ebcdf --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_boto.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-boto", + "instrumentation": "opentelemetry-instrumentation-boto==0.62b0.dev", + "libraries": [ + "boto~=2.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_boto3sqs.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_boto3sqs.py new file mode 100644 index 000000000..d02ad15f6 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_boto3sqs.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-boto3sqs", + "instrumentation": "opentelemetry-instrumentation-boto3sqs==0.62b0.dev", + "libraries": [ + "boto3 ~= 1.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_botocore.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_botocore.py new file mode 100644 index 000000000..901d94d2d --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_botocore.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-botocore", + "instrumentation": "opentelemetry-instrumentation-botocore==0.62b0.dev", + "libraries": [ + "botocore ~= 1.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_cassandra.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_cassandra.py new file mode 100644 index 000000000..1d94395de --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_cassandra.py @@ -0,0 +1,31 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-cassandra", + "instrumentation": "opentelemetry-instrumentation-cassandra==0.62b0.dev", + "libraries": [ + "cassandra-driver ~= 3.25", + "scylla-driver ~= 3.25", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_celery.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_celery.py new file mode 100644 index 000000000..ddf609a98 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_celery.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-celery", + "instrumentation": "opentelemetry-instrumentation-celery==0.62b0.dev", + "libraries": [ + "celery >= 4.0, < 6.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_click.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_click.py new file mode 100644 index 000000000..68a3f2147 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_click.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-click", + "instrumentation": "opentelemetry-instrumentation-click==0.62b0.dev", + "libraries": [ + "click >= 8.1.3, < 9.0.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_confluent_kafka.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_confluent_kafka.py new file mode 100644 index 000000000..3cabe730b --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_confluent_kafka.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-confluent-kafka", + "instrumentation": "opentelemetry-instrumentation-confluent-kafka==0.62b0.dev", + "libraries": [ + "confluent-kafka >= 1.8.2, <= 2.13.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_dbapi.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_dbapi.py new file mode 100644 index 000000000..18b120fef --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_dbapi.py @@ -0,0 +1,28 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-dbapi", + "instrumentation": "opentelemetry-instrumentation-dbapi==0.62b0.dev", + "libraries": [], + "default": True, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_django.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_django.py new file mode 100644 index 000000000..fab22233d --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_django.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-django", + "instrumentation": "opentelemetry-instrumentation-django==0.62b0.dev", + "libraries": [ + "django >= 2.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_elasticsearch.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_elasticsearch.py new file mode 100644 index 000000000..130c9eec6 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_elasticsearch.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-elasticsearch", + "instrumentation": "opentelemetry-instrumentation-elasticsearch==0.62b0.dev", + "libraries": [ + "elasticsearch >= 6.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_falcon.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_falcon.py new file mode 100644 index 000000000..122144156 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_falcon.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-falcon", + "instrumentation": "opentelemetry-instrumentation-falcon==0.62b0.dev", + "libraries": [ + "falcon >= 1.4.1, < 5.0.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_fastapi.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_fastapi.py new file mode 100644 index 000000000..a1acbf4de --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_fastapi.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-fastapi", + "instrumentation": "opentelemetry-instrumentation-fastapi==0.62b0.dev", + "libraries": [ + "fastapi ~= 0.92", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_flask.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_flask.py new file mode 100644 index 000000000..acee2283f --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_flask.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-flask", + "instrumentation": "opentelemetry-instrumentation-flask==0.62b0.dev", + "libraries": [ + "flask >= 1.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_grpc.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_grpc.py new file mode 100644 index 000000000..7add9d8f7 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_grpc.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-grpc", + "instrumentation": "opentelemetry-instrumentation-grpc==0.62b0.dev", + "libraries": [ + "grpcio >= 1.42.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_httpx.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_httpx.py new file mode 100644 index 000000000..905296c6c --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_httpx.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-httpx", + "instrumentation": "opentelemetry-instrumentation-httpx==0.62b0.dev", + "libraries": [ + "httpx >= 0.18.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_jinja2.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_jinja2.py new file mode 100644 index 000000000..653cb5f3e --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_jinja2.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-jinja2", + "instrumentation": "opentelemetry-instrumentation-jinja2==0.62b0.dev", + "libraries": [ + "jinja2 >= 2.7, < 4.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_kafka_python.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_kafka_python.py new file mode 100644 index 000000000..01f8babf9 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_kafka_python.py @@ -0,0 +1,31 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-kafka-python", + "instrumentation": "opentelemetry-instrumentation-kafka-python==0.62b0.dev", + "libraries": [ + "kafka-python >= 2.0, < 3.0", + "kafka-python-ng >= 2.0, < 3.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_logging.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_logging.py new file mode 100644 index 000000000..81d741413 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_logging.py @@ -0,0 +1,28 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-logging", + "instrumentation": "opentelemetry-instrumentation-logging==0.62b0.dev", + "libraries": [], + "default": True, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_mysql.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_mysql.py new file mode 100644 index 000000000..aebea7485 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_mysql.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-mysql", + "instrumentation": "opentelemetry-instrumentation-mysql==0.62b0.dev", + "libraries": [ + "mysql-connector-python >= 8.0, < 10.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_mysqlclient.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_mysqlclient.py new file mode 100644 index 000000000..52de16e57 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_mysqlclient.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-mysqlclient", + "instrumentation": "opentelemetry-instrumentation-mysqlclient==0.62b0.dev", + "libraries": [ + "mysqlclient < 3", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_pika.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_pika.py new file mode 100644 index 000000000..c6ea2e41c --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_pika.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-pika", + "instrumentation": "opentelemetry-instrumentation-pika==0.62b0.dev", + "libraries": [ + "pika >= 0.12.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_psycopg.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_psycopg.py new file mode 100644 index 000000000..80099f91e --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_psycopg.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-psycopg", + "instrumentation": "opentelemetry-instrumentation-psycopg==0.62b0.dev", + "libraries": [ + "psycopg >= 3.1.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_psycopg2.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_psycopg2.py new file mode 100644 index 000000000..dc6aec88b --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_psycopg2.py @@ -0,0 +1,31 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-psycopg2", + "instrumentation": "opentelemetry-instrumentation-psycopg2==0.62b0.dev", + "libraries": [ + "psycopg2 >= 2.7.3.1", + "psycopg2-binary >= 2.7.3.1", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_pymemcache.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_pymemcache.py new file mode 100644 index 000000000..187dd7e25 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_pymemcache.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-pymemcache", + "instrumentation": "opentelemetry-instrumentation-pymemcache==0.62b0.dev", + "libraries": [ + "pymemcache >= 1.3.5, < 5", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_pymongo.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_pymongo.py new file mode 100644 index 000000000..58b121294 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_pymongo.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-pymongo", + "instrumentation": "opentelemetry-instrumentation-pymongo==0.62b0.dev", + "libraries": [ + "pymongo >= 3.1, < 5.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_pymssql.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_pymssql.py new file mode 100644 index 000000000..9eda0c7bc --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_pymssql.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-pymssql", + "instrumentation": "opentelemetry-instrumentation-pymssql==0.62b0.dev", + "libraries": [ + "pymssql >= 2.1.5, < 3", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_pymysql.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_pymysql.py new file mode 100644 index 000000000..c29a975af --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_pymysql.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-pymysql", + "instrumentation": "opentelemetry-instrumentation-pymysql==0.62b0.dev", + "libraries": [ + "PyMySQL < 2", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_pyramid.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_pyramid.py new file mode 100644 index 000000000..9b91055cb --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_pyramid.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-pyramid", + "instrumentation": "opentelemetry-instrumentation-pyramid==0.62b0.dev", + "libraries": [ + "pyramid >= 1.7", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_redis.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_redis.py new file mode 100644 index 000000000..013753002 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_redis.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-redis", + "instrumentation": "opentelemetry-instrumentation-redis==0.62b0.dev", + "libraries": [ + "redis >= 2.6", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_remoulade.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_remoulade.py new file mode 100644 index 000000000..f63ac28a9 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_remoulade.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-remoulade", + "instrumentation": "opentelemetry-instrumentation-remoulade==0.62b0.dev", + "libraries": [ + "remoulade >= 0.50", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_requests.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_requests.py new file mode 100644 index 000000000..294deaacf --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_requests.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-requests", + "instrumentation": "opentelemetry-instrumentation-requests==0.62b0.dev", + "libraries": [ + "requests ~= 2.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_sqlalchemy.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_sqlalchemy.py new file mode 100644 index 000000000..852daebb3 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_sqlalchemy.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-sqlalchemy", + "instrumentation": "opentelemetry-instrumentation-sqlalchemy==0.62b0.dev", + "libraries": [ + "sqlalchemy >= 1.0.0, < 2.1.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_sqlite3.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_sqlite3.py new file mode 100644 index 000000000..6dc8825ef --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_sqlite3.py @@ -0,0 +1,28 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-sqlite3", + "instrumentation": "opentelemetry-instrumentation-sqlite3==0.62b0.dev", + "libraries": [], + "default": True, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_starlette.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_starlette.py new file mode 100644 index 000000000..f63deec04 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_starlette.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-starlette", + "instrumentation": "opentelemetry-instrumentation-starlette==0.62b0.dev", + "libraries": [ + "starlette >= 0.13", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_system_metrics.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_system_metrics.py new file mode 100644 index 000000000..0d4028d53 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_system_metrics.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-system-metrics", + "instrumentation": "opentelemetry-instrumentation-system-metrics==0.62b0.dev", + "libraries": [ + "psutil >= 5", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_threading.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_threading.py new file mode 100644 index 000000000..30b9d9983 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_threading.py @@ -0,0 +1,28 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-threading", + "instrumentation": "opentelemetry-instrumentation-threading==0.62b0.dev", + "libraries": [], + "default": True, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_tornado.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_tornado.py new file mode 100644 index 000000000..6671bdcbd --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_tornado.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-tornado", + "instrumentation": "opentelemetry-instrumentation-tornado==0.62b0.dev", + "libraries": [ + "tornado >= 5.1.1", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_tortoiseorm.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_tortoiseorm.py new file mode 100644 index 000000000..1085e6eab --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_tortoiseorm.py @@ -0,0 +1,31 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-tortoiseorm", + "instrumentation": "opentelemetry-instrumentation-tortoiseorm==0.62b0.dev", + "libraries": [ + "tortoise-orm >= 0.17.0", + "pydantic >= 1.10.2", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_urllib.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_urllib.py new file mode 100644 index 000000000..860d1d3a9 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_urllib.py @@ -0,0 +1,28 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-urllib", + "instrumentation": "opentelemetry-instrumentation-urllib==0.62b0.dev", + "libraries": [], + "default": True, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_urllib3.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_urllib3.py new file mode 100644 index 000000000..fe06cefb3 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_urllib3.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-urllib3", + "instrumentation": "opentelemetry-instrumentation-urllib3==0.62b0.dev", + "libraries": [ + "urllib3 >= 1.0.0, < 3.0.0", + ], + "default": False, +} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_wsgi.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_wsgi.py new file mode 100644 index 000000000..139f20fd2 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/opentelemetry_instrumentation_wsgi.py @@ -0,0 +1,28 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "upstream", + "package": "opentelemetry-instrumentation-wsgi", + "instrumentation": "opentelemetry-instrumentation-wsgi==0.62b0.dev", + "libraries": [], + "default": True, +} diff --git a/scripts/loongsuite/generate_loongsuite_bootstrap.py b/scripts/loongsuite/generate_loongsuite_bootstrap.py index cfd1f2b59..94217f201 100755 --- a/scripts/loongsuite/generate_loongsuite_bootstrap.py +++ b/scripts/loongsuite/generate_loongsuite_bootstrap.py @@ -15,10 +15,12 @@ # limitations under the License. """ -Generate bootstrap_gen.py for loongsuite-distro. +Generate bootstrap registry fragments for loongsuite-distro. -This script generates the libraries and default_instrumentations lists -used by loongsuite-bootstrap to install instrumentations. +This script generates one package-local registry fragment per instrumentation +package. The stable bootstrap_gen.py compatibility module imports those +fragments and exposes the libraries and default_instrumentations lists used by +loongsuite-bootstrap. Package naming and version strategy: - instrumentation-genai/* packages: renamed to loongsuite-* prefix @@ -40,8 +42,9 @@ """ import argparse -import ast +import json import logging +import re import subprocess import sys from pathlib import Path @@ -49,23 +52,107 @@ import tomli -# Allow importing sibling modules from the parent scripts/ directory -sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) -from generate_instrumentation_bootstrap import ( - independent_packages, - packages_to_exclude, -) -from otel_packaging import ( - get_instrumentation_packages as get_upstream_packages, -) - logging.basicConfig(level=logging.INFO) logger = logging.getLogger("loongsuite_bootstrap_generator") scripts_path = Path(__file__).parent root_path = scripts_path.parent.parent +SOURCE_GENAI_RENAMED = "genai-renamed" +SOURCE_UPSTREAM = "upstream" +SOURCE_LOONGSUITE = "loongsuite" + +bootstrap_dir = ( + root_path / "loongsuite-distro" / "src" / "loongsuite" / "distro" +) +registry_path = bootstrap_dir / "bootstrap_registry" +gen_path = bootstrap_dir / "bootstrap_gen.py" + +_bootstrap_gen_template = """{header} + +\"\"\"Compatibility exports for the LoongSuite bootstrap registry.\"\"\" + +from loongsuite.distro.bootstrap_registry import ( + default_instrumentations, + libraries, +) + +__all__ = [\"default_instrumentations\", \"libraries\"] +""" + +_registry_init_template = """{header} + +\"\"\"Package-scoped bootstrap registry fragments for LoongSuite distro.\"\"\" + +from __future__ import annotations + +from importlib import import_module +from pkgutil import iter_modules +from typing import Any + + +_SOURCE_SORT_ORDER = { + "genai-renamed": 0, + "upstream": 1, + "loongsuite": 2, +} + + +def _iter_registry_entries() -> list[dict[str, Any]]: + entries: list[dict[str, Any]] = [] + package_path = globals()["__path__"] + for module_info in sorted( + iter_modules(package_path), + key=lambda info: info.name, + ): + if module_info.ispkg or module_info.name.startswith(\"_\"): + continue + + module = import_module(f\"{__name__}.{module_info.name}\") + registry = getattr(module, \"REGISTRY\", None) + if registry is not None: + entries.append(registry) + + return sorted( + entries, + key=lambda registry: ( + _SOURCE_SORT_ORDER.get(registry.get("source"), 99), + registry.get("package", ""), + ), + ) + + +def load_bootstrap_registry() -> tuple[list[dict[str, str]], list[str]]: + libraries: list[dict[str, str]] = [] + default_instrumentations: list[str] = [] + + for registry in _iter_registry_entries(): + instrumentation = registry[\"instrumentation\"] + target_libraries = registry.get(\"libraries\") or [] + if not target_libraries: + default_instrumentations.append(instrumentation) + continue + + for target_library in target_libraries: + libraries.append( + { + \"library\": target_library, + \"instrumentation\": instrumentation, + } + ) -_template = """{header} + return libraries, default_instrumentations + + +libraries, default_instrumentations = load_bootstrap_registry() + +__all__ = [ + \"default_instrumentations\", + \"libraries\", + \"load_bootstrap_registry\", +] +""" + +_registry_fragment_template = """{header} # DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. # RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. @@ -74,20 +161,121 @@ # --upstream-version: {upstream_version} # --loongsuite-version: {loongsuite_version} -{source} +REGISTRY = {registry} """ -gen_path = ( - root_path - / "loongsuite-distro" - / "src" - / "loongsuite" - / "distro" - / "bootstrap_gen.py" -) + +def _registry_module_name(package_name: str) -> str: + """Return a stable Python module name for a package registry fragment.""" + normalized = package_name.replace("-", "_").replace(".", "_").lower() + return re.sub(r"[^a-z0-9_]", "_", normalized) + + +def _registry_entry(pkg: dict) -> dict: + libraries = [ + *pkg.get("instruments", []), + *pkg.get("instruments-any", []), + ] + return { + "source": pkg.get("bootstrap-source", "unknown"), + "package": pkg["name"], + "instrumentation": pkg["requirement"], + "libraries": libraries, + "default": not libraries, + } + + +def _render_python_string(value: str) -> str: + return json.dumps(value) + + +def _render_registry_literal(entry: dict) -> str: + lines = [ + "{", + f' "source": {_render_python_string(entry["source"])},', + f' "package": {_render_python_string(entry["package"])},', + ( + ' "instrumentation": ' + f"{_render_python_string(entry['instrumentation'])}," + ), + ] + + if entry["libraries"]: + lines.append(' "libraries": [') + for library in entry["libraries"]: + lines.append(f" {_render_python_string(library)},") + lines.append(" ],") + else: + lines.append(' "libraries": [],') + + lines.extend( + [ + f' "default": {entry["default"]},', + "}", + ] + ) + return "\n".join(lines) -def get_genai_packages_to_rename() -> dict[str, str]: +def _render_registry_module( + entry: dict, + header: str, + upstream_version: Optional[str], + loongsuite_version: Optional[str], +) -> str: + registry = _render_registry_literal(entry) + return _registry_fragment_template.format( + header=header, + registry=registry, + upstream_version=upstream_version or "(from source)", + loongsuite_version=loongsuite_version or "(from source)", + ) + + +def _write_registry_init(header: str) -> None: + registry_path.mkdir(parents=True, exist_ok=True) + with open(registry_path / "__init__.py", "w", encoding="utf-8") as f: + f.write(_registry_init_template.replace("{header}", header)) + + +def _write_bootstrap_gen(header: str) -> None: + gen_path.parent.mkdir(parents=True, exist_ok=True) + with open(gen_path, "w", encoding="utf-8") as f: + f.write(_bootstrap_gen_template.format(header=header)) + + +def _remove_stale_registry_fragments() -> None: + if not registry_path.exists(): + return + + for path in registry_path.glob("*.py"): + if path.name == "__init__.py" or path.name.startswith("_"): + continue + path.unlink() + + +def _load_upstream_bootstrap_context(): + # Import the upstream bootstrap helper only when full package scanning runs. + # Unit tests for the local registry rendering helpers should not need the + # upstream generator's formatting dependencies. + scripts_parent = str(Path(__file__).resolve().parent.parent) + if scripts_parent not in sys.path: + sys.path.insert(0, scripts_parent) + + from generate_instrumentation_bootstrap import ( # noqa: PLC0415 + independent_packages, + packages_to_exclude, + ) + from otel_packaging import ( # noqa: PLC0415 + get_instrumentation_packages as get_upstream_packages, + ) + + return independent_packages, packages_to_exclude, get_upstream_packages + + +def get_genai_packages_to_rename( + excluded_packages: Optional[set[str]] = None, +) -> dict[str, str]: """ Dynamically discover instrumentation-genai packages that need renaming. @@ -98,6 +286,7 @@ def get_genai_packages_to_rename() -> dict[str, str]: Dict mapping original name to new name, e.g.: {"opentelemetry-instrumentation-vertexai": "loongsuite-instrumentation-vertexai"} """ + excluded_packages = excluded_packages or set() result = {} genai_dir = root_path / "instrumentation-genai" @@ -113,7 +302,7 @@ def get_genai_packages_to_rename() -> dict[str, str]: # Rule: opentelemetry-* prefix packages should be renamed to loongsuite-* if pkg_name.startswith("opentelemetry-"): # Skip packages in upstream exclude list - if pkg_name in packages_to_exclude: + if pkg_name in excluded_packages: continue new_name = pkg_name.replace("opentelemetry-", "loongsuite-") @@ -142,9 +331,15 @@ def get_instrumentation_packages( seen_packages: dict[str, int] = {} # name -> index in packages list logger.info("Scanning instrumentation packages...") + ( + independent_packages, + packages_to_exclude, + get_upstream_packages, + ) = _load_upstream_bootstrap_context() + excluded_packages = set(packages_to_exclude) # Dynamically get packages that need renaming - genai_packages_to_rename = get_genai_packages_to_rename() + genai_packages_to_rename = get_genai_packages_to_rename(excluded_packages) # Get packages from upstream directories (instrumentation, instrumentation-genai) logger.info( @@ -156,7 +351,7 @@ def get_instrumentation_packages( pkg_name = pkg["name"] # Skip packages in exclude list (follow upstream behavior) - if pkg_name in packages_to_exclude: + if pkg_name in excluded_packages: continue # Check if this package should be renamed (instrumentation-genai with opentelemetry-* prefix) @@ -164,6 +359,7 @@ def get_instrumentation_packages( new_name = genai_packages_to_rename[pkg_name] logger.info(f"Renaming {pkg_name} -> {new_name}") pkg["name"] = new_name + pkg["bootstrap-source"] = SOURCE_GENAI_RENAMED # Use loongsuite version for renamed packages if loongsuite_version: @@ -174,6 +370,8 @@ def get_instrumentation_packages( pkg_name, new_name ) else: + pkg["bootstrap-source"] = SOURCE_UPSTREAM + # Use upstream version for opentelemetry-* packages if upstream_version and pkg_name.startswith("opentelemetry-"): pkg["requirement"] = f"{pkg_name}=={upstream_version}" @@ -216,7 +414,7 @@ def get_instrumentation_packages( pkg_name = pyproject["project"]["name"] - if pkg_name in packages_to_exclude: + if pkg_name in excluded_packages: continue # Get optional dependencies @@ -240,6 +438,7 @@ def get_instrumentation_packages( requirement = f"{pkg_name}=={version}" new_pkg = { + "bootstrap-source": SOURCE_LOONGSUITE, "name": pkg_name, "version": version, "instruments": instruments, @@ -271,7 +470,7 @@ def get_instrumentation_packages( def main(): parser = argparse.ArgumentParser( - description="Generate bootstrap_gen.py for loongsuite-distro" + description="Generate bootstrap registry fragments for loongsuite-distro" ) parser.add_argument( "--upstream-version", @@ -302,126 +501,44 @@ def main(): ) logger.info(f"Found {len(packages)} instrumentation packages") - # Build AST nodes - default_instrumentations = ast.List(elts=[]) - libraries = ast.List(elts=[]) - - logger.info("Building bootstrap configuration...") - for pkg in packages: - # If no instruments and no instruments-any, it's a default instrumentation - if not pkg.get("instruments") and not pkg.get("instruments-any"): - default_instrumentations.elts.append( - ast.Constant(value=pkg["requirement"]) - ) - else: - # Add instruments (all must be installed) - for target_pkg in pkg.get("instruments", []): - libraries.elts.append( - ast.Dict( - keys=[ - ast.Constant(value="library"), - ast.Constant(value="instrumentation"), - ], - values=[ - ast.Constant(value=target_pkg), - ast.Constant(value=pkg["requirement"]), - ], - ) - ) + entries = [_registry_entry(pkg) for pkg in packages] + default_instrumentations = [entry for entry in entries if entry["default"]] + library_mappings = sum(len(entry["libraries"]) for entry in entries) - # Add instruments-any (at least one must be installed) - for target_pkg in pkg.get("instruments-any", []): - libraries.elts.append( - ast.Dict( - keys=[ - ast.Constant(value="library"), - ast.Constant(value="instrumentation"), - ], - values=[ - ast.Constant(value=target_pkg), - ast.Constant(value=pkg["requirement"]), - ], - ) - ) + logger.info("Generating bootstrap registry fragments...") + _write_registry_init(header) + _write_bootstrap_gen(header) + _remove_stale_registry_fragments() - # Generate source code - logger.info("Generating source code...") - # Build libraries list string - libraries_lines = ["libraries = ["] - for lib_mapping in libraries.elts: - if isinstance(lib_mapping, ast.Dict): - lib_key = lib_mapping.keys[0] - instr_key = lib_mapping.keys[1] - lib_val_node = lib_mapping.values[0] - instr_val_node = lib_mapping.values[1] - - lib_key_str = ( - lib_key.value - if isinstance(lib_key, ast.Constant) - else (lib_key.s if hasattr(lib_key, "s") else "library") + written_paths: set[Path] = set() + for entry in entries: + registry_file = ( + registry_path / f"{_registry_module_name(entry['package'])}.py" + ) + if registry_file in written_paths: + raise RuntimeError( + f"duplicate bootstrap registry fragment: {registry_file}" ) - instr_key_str = ( - instr_key.value - if isinstance(instr_key, ast.Constant) - else ( - instr_key.s - if hasattr(instr_key, "s") - else "instrumentation" + written_paths.add(registry_file) + + with open(registry_file, "w", encoding="utf-8") as f: + f.write( + _render_registry_module( + entry, + header, + args.upstream_version, + args.loongsuite_version, ) ) - lib_val_str = ( - lib_val_node.value - if isinstance(lib_val_node, ast.Constant) - else (lib_val_node.s if hasattr(lib_val_node, "s") else "") - ) - instr_val_str = ( - instr_val_node.value - if isinstance(instr_val_node, ast.Constant) - else (instr_val_node.s if hasattr(instr_val_node, "s") else "") - ) - - lib_val_str = lib_val_str.replace('"', '\\"') - instr_val_str = instr_val_str.replace('"', '\\"') - libraries_lines.append(" {") - libraries_lines.append( - f' "{lib_key_str}": "{lib_val_str}",' - ) - libraries_lines.append( - f' "{instr_key_str}": "{instr_val_str}",' - ) - libraries_lines.append(" },") - libraries_lines.append("]") - - # Build default_instrumentations list string - default_lines = ["default_instrumentations = ["] - for default_instr in default_instrumentations.elts: - if isinstance(default_instr, ast.Constant): - instr_val = default_instr.value.replace('"', '\\"') - default_lines.append(f' "{instr_val}",') - default_lines.append("]") - - # Combine source - source = "\n".join(libraries_lines) + "\n\n" + "\n".join(default_lines) - - # Format with header and version info - formatted_source = _template.format( - header=header, - source=source, - upstream_version=args.upstream_version or "(from source)", - loongsuite_version=args.loongsuite_version or "(from source)", + logger.info( + "generated %d registry fragments in %s", len(entries), registry_path ) - - # Write to file - gen_path.parent.mkdir(parents=True, exist_ok=True) - with open(gen_path, "w", encoding="utf-8") as f: - f.write(formatted_source) - logger.info("generated %s", gen_path) logger.info( - " - %d default instrumentations", len(default_instrumentations.elts) + " - %d default instrumentations", len(default_instrumentations) ) - logger.info(" - %d library mappings", len(libraries.elts)) + logger.info(" - %d library mappings", library_mappings) if __name__ == "__main__": diff --git a/scripts/loongsuite/tests/test_generate_loongsuite_bootstrap.py b/scripts/loongsuite/tests/test_generate_loongsuite_bootstrap.py new file mode 100644 index 000000000..64f1565e2 --- /dev/null +++ b/scripts/loongsuite/tests/test_generate_loongsuite_bootstrap.py @@ -0,0 +1,172 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path + +SCRIPT_PATH = Path(__file__).parents[1] / "generate_loongsuite_bootstrap.py" +SPEC = importlib.util.spec_from_file_location( + "generate_loongsuite_bootstrap", + SCRIPT_PATH, +) +generate = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(generate) + + +def test_registry_module_name_normalizes_package_name(): + assert ( + generate._registry_module_name("loongsuite-instrumentation-crewai") + == "loongsuite_instrumentation_crewai" + ) + assert ( + generate._registry_module_name( + "opentelemetry-instrumentation-aiohttp-client" + ) + == "opentelemetry_instrumentation_aiohttp_client" + ) + + +def test_registry_entry_combines_instruments_and_marks_default(): + entry = generate._registry_entry( + { + "name": "loongsuite-instrumentation-crewai", + "bootstrap-source": "loongsuite", + "requirement": "loongsuite-instrumentation-crewai==0.1.0", + "instruments": ["crewai >= 0.100.0"], + "instruments-any": ["crewai-tools >= 0.40.0"], + } + ) + + assert entry == { + "source": "loongsuite", + "package": "loongsuite-instrumentation-crewai", + "instrumentation": "loongsuite-instrumentation-crewai==0.1.0", + "libraries": ["crewai >= 0.100.0", "crewai-tools >= 0.40.0"], + "default": False, + } + + default_entry = generate._registry_entry( + { + "name": "loongsuite-instrumentation-dashscope", + "bootstrap-source": "loongsuite", + "requirement": "loongsuite-instrumentation-dashscope==0.1.0", + } + ) + + assert default_entry == { + "source": "loongsuite", + "package": "loongsuite-instrumentation-dashscope", + "instrumentation": "loongsuite-instrumentation-dashscope==0.1.0", + "libraries": [], + "default": True, + } + + +def test_render_registry_module_contains_local_metadata(): + source = generate._render_registry_module( + { + "source": "loongsuite", + "package": "loongsuite-instrumentation-crewai", + "instrumentation": "loongsuite-instrumentation-crewai==0.1.0", + "libraries": ["crewai >= 0.100.0"], + "default": False, + }, + "# license", + upstream_version="0.62b0", + loongsuite_version="0.1.0", + ) + + assert "REGISTRY = {" in source + assert '"source": "loongsuite"' in source + assert '"package": "loongsuite-instrumentation-crewai"' in source + assert "--upstream-version: 0.62b0" in source + assert "--loongsuite-version: 0.1.0" in source + + +def test_registry_loader_builds_bootstrap_gen_compatible_lists(tmp_path): + package_name = "test_bootstrap_registry" + package_path = tmp_path / package_name + package_path.mkdir() + (package_path / "__init__.py").write_text( + generate._registry_init_template.replace("{header}", "# license"), + encoding="utf-8", + ) + (package_path / "loongsuite_instrumentation_crewai.py").write_text( + "REGISTRY = {\n" + " 'source': 'loongsuite',\n" + " 'package': 'loongsuite-instrumentation-crewai',\n" + " 'instrumentation': 'loongsuite-instrumentation-crewai==0.1.0',\n" + " 'libraries': ['crewai >= 0.100.0'],\n" + " 'default': False,\n" + "}\n", + encoding="utf-8", + ) + (package_path / "loongsuite_instrumentation_dashscope.py").write_text( + "REGISTRY = {\n" + " 'source': 'loongsuite',\n" + " 'package': 'loongsuite-instrumentation-dashscope',\n" + " 'instrumentation': 'loongsuite-instrumentation-dashscope==0.1.0',\n" + " 'libraries': [],\n" + " 'default': True,\n" + "}\n", + encoding="utf-8", + ) + ( + package_path / "opentelemetry_instrumentation_aiohttp_client.py" + ).write_text( + "REGISTRY = {\n" + " 'source': 'upstream',\n" + " 'package': 'opentelemetry-instrumentation-aiohttp-client',\n" + " 'instrumentation': " + "'opentelemetry-instrumentation-aiohttp-client==0.62b0.dev',\n" + " 'libraries': ['aiohttp ~= 3.0'],\n" + " 'default': False,\n" + "}\n", + encoding="utf-8", + ) + + spec = importlib.util.spec_from_file_location( + package_name, + package_path / "__init__.py", + submodule_search_locations=[str(package_path)], + ) + registry = importlib.util.module_from_spec(spec) + sys.modules[package_name] = registry + try: + spec.loader.exec_module(registry) + finally: + for module_name in list(sys.modules): + if module_name == package_name or module_name.startswith( + f"{package_name}." + ): + del sys.modules[module_name] + + assert registry.libraries == [ + { + "library": "aiohttp ~= 3.0", + "instrumentation": ( + "opentelemetry-instrumentation-aiohttp-client==0.62b0.dev" + ), + }, + { + "library": "crewai >= 0.100.0", + "instrumentation": "loongsuite-instrumentation-crewai==0.1.0", + }, + ] + assert registry.default_instrumentations == [ + "loongsuite-instrumentation-dashscope==0.1.0" + ] diff --git a/tox-loongsuite.ini b/tox-loongsuite.ini index 108c6706d..a7d561795 100644 --- a/tox-loongsuite.ini +++ b/tox-loongsuite.ini @@ -206,6 +206,7 @@ deps = util-genai: {toxinidir}/util/opentelemetry-util-genai detect-loongsuite-changes: pytest + detect-loongsuite-changes: tomli litellm: {[testenv]test_deps} litellm: -r {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-litellm/tests/test-requirements.txt @@ -314,7 +315,7 @@ commands = lint-util-genai: sh -c "cd util && pylint --rcfile ../.pylintrc opentelemetry-util-genai" ; This env covers the detector and selector scripts used by dynamic LoongSuite CI. - test-detect-loongsuite-changes: pytest {toxinidir}/.github/scripts/tests {posargs} + test-detect-loongsuite-changes: pytest {toxinidir}/.github/scripts/tests {toxinidir}/scripts/loongsuite/tests {posargs} test-loongsuite-instrumentation-litellm: pytest {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-litellm/tests {posargs} lint-loongsuite-instrumentation-litellm: python -m ruff check {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-litellm From e9b82852fcfe8534705ed22b69a9bce99ef8c80a Mon Sep 17 00:00:00 2001 From: sipercai <53717475+sipercai@users.noreply.github.com> Date: Tue, 30 Jun 2026 13:54:26 +0800 Subject: [PATCH 02/21] chore: add CrewAI changelog entry --- .../loongsuite-instrumentation-crewai/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-crewai/CHANGELOG.md b/instrumentation-loongsuite/loongsuite-instrumentation-crewai/CHANGELOG.md index 3f7104087..6cfd65b64 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-crewai/CHANGELOG.md +++ b/instrumentation-loongsuite/loongsuite-instrumentation-crewai/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Fixed + +- Keep the authentication-failure test deterministic by using fake provider + keys instead of empty keys, avoiding fallback live OpenAI calls in CI. + ([#232](https://github.com/alibaba/loongsuite-python/pull/232)) + ## Version 0.6.0 (2026-06-03) ### Breaking From 34d8a2ec6746bf6f9aa9acfedb97a41d3333dc5d Mon Sep 17 00:00:00 2001 From: "liuziming.lzm" Date: Wed, 24 Jun 2026 09:57:28 +0800 Subject: [PATCH 03/21] feat(instrumentation-microsoft-agent-framework): add plugin per execute.md Implements the hybrid "SpanProcessor + optional ReAct step patch" plan documented in llm-dev/microsoft-agent-framework/investigate/execute.md. - MicrosoftAgentFrameworkInstrumentor: enables MAF native OTel layers (force=True) and registers MAFSemanticProcessor. - MAFSemanticProcessor (span_processor.py): injects gen_ai.span.kind, gen_ai.operation.name, renames MAF private-prefix attributes to gen_ai.*, normalizes provider.name (azure_openai -> openai), backfills TTFT from streaming events, sets StatusCode.OK on success, aggregates the 6 ARMS gauges. Reuses opentelemetry.util.genai.utils.gen_ai_json_dumps (aligned with openai-agents-v2/span_processor.py:27) to coerce dict/list attribute values into JSON strings. - react_step_patch.py (opt-in via ARMS_MAF_REACT_STEP_ENABLED): emits one react step span per LLM round-trip inside FunctionInvocationLayer via ExtendedTelemetryHandler.react_step() from opentelemetry-util-genai. - config.py: env switches (master, sensitive data, react step, slow threshold). - tests: 23 passing unit tests covering span classification, metric aggregation, provider normalization, TTFT backfill, dict coercion, and react_step handler behavior. --- .../README.rst | 37 + .../pyproject.toml | 58 ++ .../microsoft_agent_framework/__init__.py | 133 ++++ .../microsoft_agent_framework/config.py | 52 ++ .../microsoft_agent_framework/package.py | 2 + .../microsoft_agent_framework/py.typed | 0 .../react_step_patch.py | 170 +++++ .../semantic_conventions.py | 116 ++++ .../span_processor.py | 657 ++++++++++++++++++ .../microsoft_agent_framework/version.py | 1 + .../tests/__init__.py | 0 .../tests/test_config.py | 38 + .../tests/test_processor.py | 311 +++++++++ .../tests/test_react_step.py | 70 ++ 14 files changed, 1645 insertions(+) create mode 100644 instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/README.rst create mode 100644 instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/pyproject.toml create mode 100644 instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/__init__.py create mode 100644 instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/config.py create mode 100644 instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/package.py create mode 100644 instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/py.typed create mode 100644 instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/react_step_patch.py create mode 100644 instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py create mode 100644 instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py create mode 100644 instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/version.py create mode 100644 instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/__init__.py create mode 100644 instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_config.py create mode 100644 instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_processor.py create mode 100644 instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_react_step.py diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/README.rst b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/README.rst new file mode 100644 index 000000000..be54e1c11 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/README.rst @@ -0,0 +1,37 @@ +============================== +Microsoft Agent Framework Instrumentation +============================== + +This package provides OpenTelemetry instrumentation for +`Microsoft Agent Framework `_ +(``agent-framework-core``). + +It implements the hybrid "SpanProcessor + optional ReAct step patch" plan +described in ``llm-dev/microsoft-agent-framework/investigate/execute.md``: + +* ``MAFSemanticProcessor`` enriches the native OTel spans emitted by MAF's + ``ChatTelemetryLayer`` / ``EmbeddingTelemetryLayer`` / + ``AgentTelemetryLayer`` / workflow helpers with the ARMS GenAI semantic + conventions (``gen_ai.span.kind``, ``gen_ai.operation.name``, normalized + attribute names, ``gen_ai.response.time_to_first_token``, ``StatusCode.OK`` + on success, etc.) and aggregates the 6 ARMS gauges. +* ``react_step_patch`` (opt-in via ``ARMS_MAF_REACT_STEP_ENABLED=true``) + wraps ``FunctionInvocationLayer.get_response`` so that each LLM round-trip + inside the ReAct loop emits one ``react step`` span via + ``ExtendedTelemetryHandler.react_step()`` from ``opentelemetry-util-genai``. + +Truncation / PII helpers are reused from ``opentelemetry.util.genai.utils`` +(``gen_ai_json_dumps``), aligned with +``instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/.../span_processor.py``. + +Configuration +============ + +============================ ========== ========================================== +Env Default Description +============================ ========== ========================================== +``ARMS_MAF_INSTRUMENTATION_ENABLED`` ``true`` Master switch; ``false`` disables instrumentation. +``ARMS_MAF_SENSITIVE_DATA_ENABLED`` ``false`` Capture inputs/outputs (linked to MAF's ``ENABLE_SENSITIVE_DATA``). +``ARMS_MAF_REACT_STEP_ENABLED`` ``false`` Emit ``react step`` spans (opt-in). +``ARMS_MAF_SLOW_THRESHOLD_MS`` ``1000`` Slow-call threshold in ms. +============================ ========== ========================================== diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/pyproject.toml b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/pyproject.toml new file mode 100644 index 000000000..ffce400f7 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/pyproject.toml @@ -0,0 +1,58 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-instrumentation-microsoft-agent-framework" +dynamic = ["version"] +description = "OpenTelemetry instrumentation for Microsoft Agent Framework (agent-framework-core)" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.9" +authors = [ + { name = "ARMS LoongSuite Authors" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dependencies = [ + "opentelemetry-api >= 1.37", + "opentelemetry-instrumentation >= 0.58b0", + "opentelemetry-semantic-conventions >= 0.58b0", + "opentelemetry-util-genai", + "wrapt >= 1.14", +] + +[project.optional-dependencies] +instruments = [ + "agent-framework-core >= 1.0.0", +] + +[project.entry-points.opentelemetry_instrumentor] +microsoft_agent_framework = "opentelemetry.instrumentation.microsoft_agent_framework:MicrosoftAgentFrameworkInstrumentor" + +[project.urls] +Homepage = "https://github.com/alibaba/loongsuite-python-agent" +Repository = "https://github.com/alibaba/loongsuite-python-agent" + +[tool.hatch.version] +path = "src/opentelemetry/instrumentation/microsoft_agent_framework/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", + "/README.rst", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/__init__.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/__init__.py new file mode 100644 index 000000000..fda3ab282 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/__init__.py @@ -0,0 +1,133 @@ +"""OpenTelemetry instrumentation for Microsoft Agent Framework. + +This instrumentor enables MAF's built-in OTel telemetry (``enable_instrumentation`` +with ``force=True`` so a sticky user-disable does not block us) and registers a +:class:`~.span_processor.MAFSemanticProcessor` that enriches MAF's native +spans to align with the ARMS GenAI semantic conventions. + +The optional ReAct step patch (``ARMS_MAF_REACT_STEP_ENABLED=true``) wraps the +``FunctionInvocationLayer.get_response`` ReAct loop with +``ExtendedTelemetryHandler.react_step()`` spans — direct ``start_as_current_span`` +calls are not used, per the plugin's hard constraints. + +Usage:: + + from opentelemetry.instrumentation.microsoft_agent_framework import ( + MicrosoftAgentFrameworkInstrumentor, + ) + MicrosoftAgentFrameworkInstrumentor().instrument() +""" + +from __future__ import annotations + +import logging +from typing import Any, Collection, Optional + +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.trace import get_tracer_provider + +from .config import ( + get_slow_threshold_ms, + is_instrumentation_enabled, + is_metrics_enabled, + is_react_step_enabled, + is_sensitive_data_enabled, +) +from .package import _instruments +from .react_step_patch import apply_react_step_patch, revert_react_step_patch +from .span_processor import MAFSemanticProcessor +from .version import __version__ + +__all__ = ["MicrosoftAgentFrameworkInstrumentor", "__version__"] + +logger = logging.getLogger(__name__) + + +class MicrosoftAgentFrameworkInstrumentor(BaseInstrumentor): + """Instrumentor for Microsoft Agent Framework (``agent-framework-core``).""" + + def __init__(self) -> None: + super().__init__() + self._processor: Optional[MAFSemanticProcessor] = None + self._react_applied: bool = False + + def instrumentation_dependencies(self) -> Collection[str]: + return _instruments + + def _instrument(self, **kwargs: Any) -> None: + if not is_instrumentation_enabled(default=True): + logger.info( + "Microsoft Agent Framework instrumentation disabled by env " + "(ARMS_MAF_INSTRUMENTATION_ENABLED=false)." + ) + return + if self._processor is not None: + return + + tracer_provider = kwargs.get("tracer_provider") or get_tracer_provider() + meter_provider = kwargs.get("meter_provider") + + # 1) Enable MAF's built-in OTel instrumentation. ``force=True`` clears + # any sticky disable previously set by ``disable_instrumentation()`` + # so our instrumentation always takes effect. ``enable_sensitive_data`` + # is wired to the ARMS_MAF_SENSITIVE_DATA_ENABLED env (default False — + # PII/data redaction by default, per ARMS privacy guardrails). + sensitive = bool( + kwargs.get( + "enable_sensitive_data", is_sensitive_data_enabled(default=False) + ) + ) + try: + from agent_framework.observability import enable_instrumentation + + enable_instrumentation(enable_sensitive_data=sensitive, force=True) + except (ImportError, AttributeError, TypeError) as exc: + logger.warning( + "Could not enable MAF native instrumentation: %s. " + "Spans will not be emitted by MAF; the SpanProcessor will " + "still be registered but inert.", + exc, + ) + + # 2) Register the semantic SpanProcessor. MAF uses the standard OTel + # TracerProvider (it does not have its own multi-processor), so + # ``add_span_processor`` is the right hook. + processor = MAFSemanticProcessor( + meter_provider=meter_provider, + slow_threshold_ms=get_slow_threshold_ms(), + metrics_enabled=is_metrics_enabled(default=True), + capture_sensitive_data=sensitive, + ) + try: + tracer_provider.add_span_processor(processor) + except Exception as exc: + logger.warning("add_span_processor failed: %s", exc) + raise + self._processor = processor + + # 3) Optional ReAct step patch (default OFF). + react_enabled = bool( + kwargs.get("react_step_enabled", is_react_step_enabled(default=False)) + ) + if react_enabled: + try: + apply_react_step_patch(tracer_provider) + self._react_applied = True + except (AttributeError, ImportError, TypeError) as exc: + logger.warning("ReAct step patch skipped: %s", exc) + + def _uninstrument(self, **kwargs: Any) -> None: + if self._react_applied: + revert_react_step_patch() + self._react_applied = False + if self._processor is not None: + try: + self._processor.shutdown() + except Exception as exc: # pragma: no cover - defensive + logger.debug("processor shutdown error: %s", exc) + self._processor = None + + # We intentionally do NOT call ``disable_instrumentation()`` — that + # would set MAF's sticky ``_user_disabled`` flag and prevent the user + # from re-enabling later without ``force=True``. Respects the user's + # own MAF observability state. diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/config.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/config.py new file mode 100644 index 000000000..af65bf71b --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/config.py @@ -0,0 +1,52 @@ +"""Environment configuration for Microsoft Agent Framework instrumentation.""" + +from __future__ import annotations + +import os +from typing import Any + + +def _resolve_bool(value: Any, default: bool) -> bool: + if value is None: + return default + if isinstance(value, bool): + return value + text = str(value).strip().lower() + if text in {"true", "1", "yes", "on"}: + return True + if text in {"false", "0", "no", "off"}: + return False + return default + + +ENV_INSTRUMENTATION_ENABLED = "ARMS_MAF_INSTRUMENTATION_ENABLED" +ENV_SENSITIVE_DATA_ENABLED = "ARMS_MAF_SENSITIVE_DATA_ENABLED" +ENV_REACT_STEP_ENABLED = "ARMS_MAF_REACT_STEP_ENABLED" +ENV_SLOW_THRESHOLD_MS = "ARMS_MAF_SLOW_THRESHOLD_MS" +ENV_METRICS_ENABLED = "ARMS_MAF_METRICS_ENABLED" + + +def is_instrumentation_enabled(default: bool = True) -> bool: + return _resolve_bool(os.getenv(ENV_INSTRUMENTATION_ENABLED), default=default) + + +def is_sensitive_data_enabled(default: bool = False) -> bool: + return _resolve_bool(os.getenv(ENV_SENSITIVE_DATA_ENABLED), default=default) + + +def is_react_step_enabled(default: bool = False) -> bool: + return _resolve_bool(os.getenv(ENV_REACT_STEP_ENABLED), default=default) + + +def is_metrics_enabled(default: bool = True) -> bool: + return _resolve_bool(os.getenv(ENV_METRICS_ENABLED), default=default) + + +def get_slow_threshold_ms(default: int = 1000) -> int: + raw = os.getenv(ENV_SLOW_THRESHOLD_MS) + if raw is None: + return default + try: + return int(raw) + except (TypeError, ValueError): + return default diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/package.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/package.py new file mode 100644 index 000000000..144e533ec --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/package.py @@ -0,0 +1,2 @@ +_instruments = ("agent-framework-core >= 1.0.0",) +_supports_metrics = True diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/py.typed b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/react_step_patch.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/react_step_patch.py new file mode 100644 index 000000000..f6cb50158 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/react_step_patch.py @@ -0,0 +1,170 @@ +"""Optional ReAct step monkey patch for Microsoft Agent Framework. + +Emits one ``react step`` span per LLM round-trip inside the +``FunctionInvocationLayer.get_response`` ReAct loop. Uses +``ExtendedTelemetryHandler.react_step()`` from ``opentelemetry.util.genai`` — +direct ``start_as_current_span`` calls are explicitly forbidden by the +plugin's hard constraints. + +Patch strategy (minimal, robust): +- Wrap ``FunctionInvocationLayer.get_response`` to set a per-call ContextVar + ``_maf_react_loop_active=True`` and reset the per-call step counter to 0. +- Wrap ``ChatTelemetryLayer.get_response`` (the LLM-call entry) so that when + invoked inside a react-loop scope, the call is wrapped with + ``handler.react_step(ReactStepInvocation(round=counter))``. Each LLM call + inside the ReAct loop = one ReAct iteration = one ``react step`` span. + The LLM span (already emitted by MAF's ``ChatTelemetryLayer``) becomes a + child of the react_step span, matching the spec hierarchy + ``AGENT > STEP > LLM``. + +The patch is OFF by default (``ARMS_MAF_REACT_STEP_ENABLED=false``). It only +imports ``agent_framework`` internals when actually applied, so import-time +failure of a renamed MAF internal does not break the rest of instrumentation. +""" + +from __future__ import annotations + +import contextvars +import logging +from typing import Any + +logger = logging.getLogger(__name__) + +# Per-call react-loop state. Set on entry to FunctionInvocationLayer.get_response +# so the ChatTelemetryLayer wrapper knows to open a react_step span around the +# LLM call. Stored as a token so we can reset cleanly on exit. +_maf_react_loop_active: contextvars.ContextVar[bool] = contextvars.ContextVar( + "_maf_react_loop_active", default=False +) +_maf_react_step_counter: contextvars.ContextVar[int] = contextvars.ContextVar( + "_maf_react_step_counter", default=0 +) + +_applied = False +_original_fil_get_response: Any = None +_original_chat_get_response: Any = None +_handler: Any = None + + +def _get_extended_handler(tracer_provider: Any = None) -> Any: + """Return the singleton ExtendedTelemetryHandler, creating it on first use.""" + global _handler + if _handler is None: + from opentelemetry.util.genai.extended_handler import ( + get_extended_telemetry_handler, + ) + + _handler = get_extended_telemetry_handler(tracer_provider=tracer_provider) + return _handler + + +def apply_react_step_patch(tracer_provider: Any = None) -> None: + """Apply the ReAct step patch. Idempotent; safe to call multiple times.""" + global _applied, _original_fil_get_response, _original_chat_get_response + if _applied: + return + + try: + import wrapt # type: ignore + from agent_framework._tools import FunctionInvocationLayer # type: ignore + from agent_framework.observability import ChatTelemetryLayer # type: ignore + except (ImportError, AttributeError) as exc: + logger.warning( + "ReAct step patch skipped: MAF internals not found (%s). " + "This is expected if agent-framework version changed.", + exc, + ) + return + + handler = _get_extended_handler(tracer_provider) + + @wrapt.wrap_function_wrapper(FunctionInvocationLayer, "get_response") + async def _fil_wrapper(wrapped, self, args, kwargs): # type: ignore[no-untyped-def] + # Outer function is async per MAF's signature (overloads collapse to + # one async implementation). Set the react-loop scope for the duration + # of the call. + token_active = _maf_react_loop_active.set(True) + token_counter = _maf_react_step_counter.set(0) + try: + return await wrapped(*args, **kwargs) + finally: + _maf_react_loop_active.reset(token_active) + _maf_react_step_counter.reset(token_counter) + + @wrapt.wrap_function_wrapper(ChatTelemetryLayer, "get_response") + async def _chat_wrapper(wrapped, self, args, kwargs): # type: ignore[no-untyped-def] + if not _maf_react_loop_active.get(): + return await wrapped(*args, **kwargs) + + # Each LLM call within the ReAct loop = one step. + round_num = _maf_react_step_counter.get() + 1 + token_counter = _maf_react_step_counter.set(round_num) + + from opentelemetry.util.genai.extended_types import ReactStepInvocation + + step_inv = ReactStepInvocation(round=round_num) + try: + with handler.react_step(step_inv) as step: + try: + result = await wrapped(*args, **kwargs) + # Best-effort finish_reason extraction from the response. + finish = _extract_finish_reason(result) + if finish is not None: + step.finish_reason = finish + return result + except Exception as exc: + step.finish_reason = "error" + raise + finally: + _maf_react_step_counter.reset(token_counter) + + _original_fil_get_response = FunctionInvocationLayer.get_response + _original_chat_get_response = ChatTelemetryLayer.get_response + _applied = True + logger.info("MAF ReAct step patch applied (handler.react_step).") + + +def _extract_finish_reason(result: Any) -> Any: + """Best-effort extraction of a finish_reason string from a ChatResponse.""" + try: + # MAF ChatResponse.messages[-1].finish_reason or choices[0].finish_reason + messages = getattr(result, "messages", None) + if messages: + last = messages[-1] + fr = getattr(last, "finish_reason", None) + if fr: + return fr + choices = getattr(result, "choices", None) + if choices: + fr = getattr(choices[0], "finish_reason", None) + if fr: + return fr + except Exception: + return None + return None + + +def revert_react_step_patch() -> None: + """Revert the ReAct step patch. Safe to call even if not applied.""" + global _applied, _original_fil_get_response, _original_chat_get_response + if not _applied: + return + try: + from agent_framework._tools import FunctionInvocationLayer # type: ignore + from agent_framework.observability import ChatTelemetryLayer # type: ignore + + if _original_fil_get_response is not None: + try: + FunctionInvocationLayer.get_response = _original_fil_get_response # type: ignore[assignment] + except Exception: + pass + if _original_chat_get_response is not None: + try: + ChatTelemetryLayer.get_response = _original_chat_get_response # type: ignore[assignment] + except Exception: + pass + except (ImportError, AttributeError): + pass + _applied = False + _original_fil_get_response = None + _original_chat_get_response = None diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py new file mode 100644 index 000000000..f9c033f0f --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py @@ -0,0 +1,116 @@ +"""Semantic convention constants and MAF→ARMS attribute mapping table. + +Centralizes: +- ``gen_ai.span.kind`` enum values (per ARMS ``gen-ai.md``). +- ``gen_ai.operation.name`` enum values. +- MAF private-prefix attribute → ``gen_ai.*`` rename map (kept as a local constant, + aligned with the pattern in + ``opentelemetry-instrumentation-openai-agents-v2/.../span_processor.py``). +""" + +from __future__ import annotations + +from typing import Final + + +class GenAISpanKind: + """``gen_ai.span.kind`` enumeration (ARMS gen-ai.md).""" + + AGENT = "AGENT" + LLM = "LLM" + TOOL = "TOOL" + EMBEDDING = "EMBEDDING" + CHAIN = "CHAIN" + TASK = "TASK" + STEP = "STEP" + ENTRY = "ENTRY" + RETRIEVER = "RETRIEVER" + RERANKER = "RERANKER" + CLIENT = "CLIENT" + + +class GenAIOperation: + """``gen_ai.operation.name`` enumeration (ARMS gen-ai.md).""" + + CHAT = "chat" + TEXT_COMPLETION = "text_completion" + GENERATE_CONTENT = "generate_content" + EMBEDDINGS = "embeddings" + EXECUTE_TOOL = "execute_tool" + CREATE_AGENT = "create_agent" + INVOKE_AGENT = "invoke_agent" + RETRIEVAL = "retrieval" + WORKFLOW = "workflow" + TASK = "task" + REACT = "react" + MCP = "mcp" + + +# MAF span-name prefixes (from observability.py OtelAttr) — used to classify a +# span when ``gen_ai.operation.name`` is not already set on it. +MAF_SPAN_NAME_PREFIXES: Final[dict[str, str]] = { + "chat ": GenAIOperation.CHAT, + "embeddings ": GenAIOperation.EMBEDDINGS, + "execute_tool ": GenAIOperation.EXECUTE_TOOL, + "invoke_agent ": GenAIOperation.INVOKE_AGENT, + "create_agent ": GenAIOperation.CREATE_AGENT, + "workflow.run": GenAIOperation.WORKFLOW, + "workflow.build": GenAIOperation.WORKFLOW, + "message.send": GenAIOperation.WORKFLOW, + "executor.process": GenAIOperation.WORKFLOW, + "edge_group.process": GenAIOperation.WORKFLOW, + "react step": GenAIOperation.REACT, +} + +# MAF writes some attributes with its own private prefix (no ``gen_ai.`` prefix). +# We rename them to the ARMS-spec key in ``MAFSemanticProcessor.on_end`` so that +# downstream platforms see a single canonical key. The mapping is idempotent — +# if MAF later writes the canonical key directly, the rename becomes a no-op +# because the source key won't be present. +MAF_ATTR_RENAME_MAP: Final[dict[str, str]] = { + # Workflow / chain attributes (observability.py:247-281) + "workflow.id": "gen_ai.workflow.id", + "workflow.name": "gen_ai.workflow.name", + "workflow.description": "gen_ai.workflow.description", + "workflow.definition": "gen_ai.workflow.definition", + "workflow_builder.name": "gen_ai.workflow.builder.name", + "workflow_builder.description": "gen_ai.workflow.builder.description", + # Executor / task attributes (observability.py:266-272) + "executor.id": "gen_ai.task.name", + "executor.type": "gen_ai.task.type", + # Edge group attributes (observability.py:270-274) + "edge_group.type": "gen_ai.edge_group.type", + "edge_group.id": "gen_ai.edge_group.id", + # Message attributes (observability.py:276-281) + "message.source_id": "gen_ai.message.source_id", + "message.target_id": "gen_ai.message.target_id", + "message.type": "gen_ai.message.type", + "message.payload_type": "gen_ai.message.payload_type", + "message.destination_executor_id": "gen_ai.message.destination_executor_id", +} + +# Provider name normalization — collapse MAF-specific provider spellings to the +# canonical OTel/ARMS value to avoid dimension sprawl in metrics. +PROVIDER_NAME_NORMALIZE: Final[dict[str, str]] = { + "azure_openai": "openai", + "azure_ai_openai": "openai", + "azure.openai": "openai", + "microsoft.agent_framework": "openai", +} + +# Attribute keys we read off the span. Centralized so tests can import them. +GEN_AI_SPAN_KIND = "gen_ai.span.kind" +GEN_AI_OPERATION_NAME = "gen_ai.operation.name" +GEN_AI_PROVIDER_NAME = "gen_ai.provider.name" +GEN_AI_REQUEST_MODEL = "gen_ai.request.model" +GEN_AI_RESPONSE_MODEL = "gen_ai.response.model" +GEN_AI_RESPONSE_TTFT = "gen_ai.response.time_to_first_token" +GEN_AI_USER_TTFT = "gen_ai.user.time_to_first_token" +GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens" +GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens" +GEN_AI_REACT_ROUND = "gen_ai.react.round" +GEN_AI_REACT_FINISH_REASON = "gen_ai.react.finish_reason" +GEN_AI_FRAMEWORK = "gen_ai.framework" +ERROR_TYPE = "error.type" + +FRAMEWORK_NAME = "microsoft-agent-framework" diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py new file mode 100644 index 000000000..a9860cb0f --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py @@ -0,0 +1,657 @@ +"""``MAFSemanticProcessor`` — a ``SpanProcessor`` that enriches Microsoft Agent +Framework's native OTel spans to align with the ARMS GenAI semantic conventions +(``/apsara/semantic-conventions/arms_docs/trace/gen-ai.md``). + +MAF already emits OTel spans via its telemetry layers (``ChatTelemetryLayer``, +``EmbeddingTelemetryLayer``, ``AgentTelemetryLayer``, workflow span helpers). +This processor: + +1. Injects ``gen_ai.span.kind`` (MAF does not set it). +2. Renames MAF private-prefix attributes (``workflow.id`` → ``gen_ai.workflow.id``, + ``executor.id`` → ``gen_ai.task.name`` …) per the local mapping table in + :mod:`semantic_conventions`. +3. Backfills ``gen_ai.response.time_to_first_token`` from the first streaming + chunk event timestamp. +4. Normalizes ``gen_ai.provider.name`` (``azure_openai`` → ``openai``). +5. Sets ``StatusCode.OK`` on successful spans (MAF only sets ``ERROR``). +6. Aggregates 6 ARMS gauges (``genai_calls_count`` / ``genai_calls_duration_seconds`` + / ``genai_calls_error_count`` / ``genai_calls_slow_count`` / ``genai_llm_first_token_seconds`` + / ``genai_llm_usage_tokens``) in-process, exposed via observable gauges. + +Truncation / PII helpers are reused from ``opentelemetry.util.genai.utils`` +(``gen_ai_json_dumps``) — aligned with the pattern at +``instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/.../span_processor.py:27``. +""" + +from __future__ import annotations + +import logging +from collections import defaultdict +from typing import Any, Dict, Optional, Tuple + +from opentelemetry.context import Context +from opentelemetry.metrics import ObservableGauge, get_meter +from opentelemetry.sdk.trace import SpanProcessor +from opentelemetry.sdk.trace import TracerProvider # noqa: F401 (typing hint) +from opentelemetry.trace import Span as OtelSpan, Status, StatusCode +from opentelemetry.trace.span import TraceState # noqa: F401 +from opentelemetry.util.genai.utils import gen_ai_json_dumps +from opentelemetry.util.types import AttributeValue + +from .semantic_conventions import ( + ERROR_TYPE, + FRAMEWORK_NAME, + GEN_AI_FRAMEWORK, + GEN_AI_OPERATION_NAME, + GEN_AI_PROVIDER_NAME, + GEN_AI_REACT_ROUND, + GEN_AI_REQUEST_MODEL, + GEN_AI_RESPONSE_MODEL, + GEN_AI_RESPONSE_TTFT, + GEN_AI_SPAN_KIND, + GEN_AI_USAGE_INPUT_TOKENS, + GEN_AI_USAGE_OUTPUT_TOKENS, + GEN_AI_USER_TTFT, + GenAIOperation, + GenAISpanKind, + MAF_ATTR_RENAME_MAP, + MAF_SPAN_NAME_PREFIXES, + PROVIDER_NAME_NORMALIZE, +) + +logger = logging.getLogger(__name__) + +# Span-name prefixes emitted by MAF (observability.py). Used to classify a span +# when ``gen_ai.operation.name`` is not already set (e.g. workflow spans). +_AGENT_PREFIX = "invoke_agent" +_CHAT_PREFIX = "chat " +_EMBEDDING_PREFIX = "embeddings " +_TOOL_PREFIX = "execute_tool " +_REACT_STEP_NAME = "react step" +_WORKFLOW_RUN = "workflow.run" +_WORKFLOW_BUILD = "workflow.build" +_MESSAGE_SEND = "message.send" +_EXECUTOR_PROCESS = "executor.process" +_EDGE_GROUP_PROCESS = "edge_group.process" + + +def _attr_value(span: Any, key: str) -> Any: + """Read an attribute from a live Span or ReadableSpan, tolerating both.""" + attrs = getattr(span, "_attributes", None) + if attrs is not None: + try: + return attrs.get(key) + except Exception: + pass + try: + return span.attributes.get(key) # type: ignore[union-attr] + except Exception: + return None + + +def _safe_dumps(obj: Any) -> Optional[str]: + """Serialize ``obj`` via the shared ``gen_ai_json_dumps`` helper. + + Reuses the truncation / PII / canonicalization logic from + ``opentelemetry.util.genai.utils`` (the same path as + ``openai-agents-v2/span_processor.py:27`` / ``safe_json_dumps`` at + ``:370``). Falls back to ``str(obj)`` if JSON serialization fails. + """ + try: + return gen_ai_json_dumps(obj) + except (TypeError, ValueError): + return str(obj) + + +_PRIMITIVE_ATTR_TYPES = (str, bool, int, float) + + +def _coerce_attr_value(value: Any) -> Any: + """Coerce ``value`` to an OTel-compatible ``AttributeValue``. + + OTel attributes accept ``str | bool | int | float`` and sequences of those. + MAF sometimes writes dict / nested-list values under its private prefixes + (``workflow.definition``, ``message.payload-type`` contents …) which the + SDK would silently drop. We dump those to a JSON string via the shared + ``gen_ai_json_dumps`` helper so the data still reaches exporters. + """ + if value is None: + return None + if isinstance(value, _PRIMITIVE_ATTR_TYPES): + return value + if isinstance(value, (list, tuple)): + coerced = [_coerce_attr_value(v) for v in value] + if all(isinstance(v, _PRIMITIVE_ATTR_TYPES) for v in coerced): + return coerced + return _safe_dumps(value) + if isinstance(value, dict): + return _safe_dumps(value) + return _safe_dumps(value) + + +def _set_attr(live_span: OtelSpan, key: str, value: Any) -> None: + """Write an attribute onto the span even after it has been ended. + + The OTel SDK's public ``Span.set_attribute`` silently no-ops once + ``Span.end()`` has been called (``is_recording()`` is False by the time + ``on_end`` runs). To enrich spans in ``on_end`` we mutate the live span's + private ``_attributes`` dict directly under its lock — same approach as + the OpenInference processor. Safe because ``on_end`` runs synchronously + on the calling thread after the span has ended, so no concurrent writers + remain. + """ + coerced = _coerce_attr_value(value) + if coerced is None: + return + attrs = getattr(live_span, "_attributes", None) + lock = getattr(live_span, "_lock", None) + if attrs is None: + try: + live_span.set_attribute(key, coerced) + except Exception: + pass + return + try: + if lock is not None: + with lock: + attrs[key] = coerced + else: + attrs[key] = coerced + except Exception as exc: # pragma: no cover - defensive + logger.debug("set_attribute(%s) failed: %s", key, exc) + + +def _rename_maf_attrs(live_span: OtelSpan, readable: Any) -> list[str]: + """Rename MAF-private attributes to ``gen_ai.*`` canonical keys. + + Returns the list of canonical keys that were written. The original MAF key + is best-effort removed from ``_attributes`` (private API; guarded by + try/except). Removal failures are harmless — the canonical key is what + downstream platforms read. + """ + renamed: list[str] = [] + attrs = getattr(live_span, "_attributes", None) + lock = getattr(live_span, "_lock", None) + for old_key, new_key in MAF_ATTR_RENAME_MAP.items(): + if attrs is not None: + try: + if lock is not None: + with lock: + present = old_key in attrs + value = attrs.get(old_key) if present else None + else: + present = old_key in attrs + value = attrs.get(old_key) if present else None + except Exception: + continue + if not present: + continue + _set_attr(live_span, new_key, value) + renamed.append(new_key) + # best-effort removal of the old (private) key + try: + if lock is not None: + with lock: + attrs.pop(old_key, None) + else: + attrs.pop(old_key, None) + except Exception: + pass + else: + # No live attrs; fall back to readable.attributes (read-only) + readable_attrs = getattr(readable, "attributes", None) or {} + if old_key in readable_attrs: + _set_attr(live_span, new_key, readable_attrs[old_key]) + renamed.append(new_key) + return renamed + + +def _normalize_provider(value: Any) -> Optional[str]: + if not value: + return None + if not isinstance(value, str): + value = str(value) + return PROVIDER_NAME_NORMALIZE.get(value, value) + + +def _classify_span( + name: str, operation: Optional[str], readable: Any +) -> Tuple[str, str]: + """Return ``(span_kind, operation_name)`` for a span. + + Classification priority: + 1. Existing ``gen_ai.operation.name`` (set by MAF for chat/embeddings/tool/agent). + 2. Span-name prefix matching (workflow spans have no operation.name from MAF). + 3. ``react step`` literal name (emitted by our react_step patch). + """ + if operation: + op = operation + else: + op = "" + for prefix, mapped in MAF_SPAN_NAME_PREFIXES.items(): + if name.startswith(prefix): + op = mapped + break + if not op: + if name == _REACT_STEP_NAME: + op = GenAIOperation.REACT + else: + op = GenAIOperation.WORKFLOW # safe default for MAF internal spans + + if op == GenAIOperation.CHAT or op == GenAIOperation.TEXT_COMPLETION or op == GenAIOperation.GENERATE_CONTENT: + return GenAISpanKind.LLM, op + if op == GenAIOperation.EMBEDDINGS: + return GenAISpanKind.EMBEDDING, op + if op == GenAIOperation.EXECUTE_TOOL: + return GenAISpanKind.TOOL, op + if op == GenAIOperation.CREATE_AGENT: + return GenAISpanKind.AGENT, op + if op == GenAIOperation.INVOKE_AGENT: + return GenAISpanKind.AGENT, op + if op == GenAIOperation.REACT: + return GenAISpanKind.STEP, op + if op == GenAIOperation.RETRIEVAL: + return GenAISpanKind.RETRIEVER, op + if op == GenAIOperation.WORKFLOW or op == GenAIOperation.TASK: + # ``executor.process`` splits by ``executor.type``: + # FunctionExecutor -> TASK + # AgentExecutor -> AGENT (kind) + invoke_agent (op) + # anything else -> CHAIN + if name.startswith(_EXECUTOR_PROCESS): + executor_type = _attr_value(readable, "executor.type") + if isinstance(executor_type, str): + et = executor_type.lower() + if "function" in et: + return GenAISpanKind.TASK, GenAIOperation.TASK + if "agent" in et: + return GenAISpanKind.AGENT, GenAIOperation.INVOKE_AGENT + return GenAISpanKind.CHAIN, op + if op == GenAIOperation.MCP: + return GenAISpanKind.CLIENT, op + return GenAISpanKind.CHAIN, op + + +def _ttft_from_events(readable: Any) -> Optional[int]: + """Backfill ``gen_ai.response.time_to_first_token`` (ns) from the first + streaming chunk event timestamp. + + MAF emits streaming chunks as span events; the first event's timestamp + minus the span start time is the TTFT. + """ + events = getattr(readable, "events", None) or () + if not events: + return None + start_time = getattr(readable, "start_time", None) + first_ts = None + for ev in events: + ts = getattr(ev, "timestamp", None) + if ts is None: + ts = ev.get("timestamp") if isinstance(ev, dict) else None + if ts is not None: + first_ts = ts + break + if first_ts is None or start_time is None: + return None + try: + return int(first_ts - start_time) + except (TypeError, ValueError): + return None + + +# ---------- Metrics aggregation ---------- + + +class _Counters: + """In-process counters keyed by ``(model, span_kind[, usage_type])``. + + Designed to be cheap to update in ``on_end`` and read out by observable + gauge callbacks. Single-process only — multi-process deployments need + per-process reporters (documented in README). + """ + + __slots__ = ( + "calls_count", + "calls_duration_ns_sum", + "calls_error_count", + "calls_slow_count", + "llm_first_token_ns_sum", + "llm_first_token_count", + "llm_usage_input_tokens", + "llm_usage_output_tokens", + ) + + def __init__(self) -> None: + self.calls_count: Dict[Tuple[str, str], int] = defaultdict(int) + self.calls_duration_ns_sum: Dict[Tuple[str, str], int] = defaultdict(int) + self.calls_error_count: Dict[Tuple[str, str], int] = defaultdict(int) + self.calls_slow_count: Dict[Tuple[str, str], int] = defaultdict(int) + self.llm_first_token_ns_sum: Dict[Tuple[str, str], int] = defaultdict(int) + self.llm_first_token_count: Dict[Tuple[str, str], int] = defaultdict(int) + self.llm_usage_input_tokens: Dict[Tuple[str, str], int] = defaultdict(int) + self.llm_usage_output_tokens: Dict[Tuple[str, str], int] = defaultdict(int) + + +class MAFSemanticProcessor(SpanProcessor): + """SpanProcessor that injects ARMS GenAI semantic conventions into MAF spans.""" + + def __init__( + self, + meter_provider: Any = None, + slow_threshold_ms: int = 1000, + metrics_enabled: bool = True, + capture_sensitive_data: bool = False, + ) -> None: + self._live_spans: Dict[str, OtelSpan] = {} + self._span_parents: Dict[str, Optional[str]] = {} + self._slow_threshold_ns = int(slow_threshold_ms) * 1_000_000 + self._capture_sensitive = capture_sensitive_data + self._counters = _Counters() + self._meter = None + self._gauges: list[ObservableGauge] = [] + self._metrics_enabled = metrics_enabled + if metrics_enabled: + try: + self._init_metrics(meter_provider) + except Exception as exc: # pragma: no cover - defensive + logger.warning("MAF metrics disabled: %s", exc) + self._metrics_enabled = False + + # ---- metrics ---- + def _init_metrics(self, meter_provider: Any) -> None: + self._meter = get_meter( + "opentelemetry.instrumentation.microsoft_agent_framework", + meter_provider=meter_provider, + ) + c = self._counters + + def _calls_cb(options): + for (model, kind), count in c.calls_count.items(): + yield _obs( + count, + {"modelName": model or "unknown", "spanKind": kind}, + ) + + def _duration_cb(options): + for (model, kind), total in c.calls_duration_ns_sum.items(): + count = max(c.calls_count.get((model, kind), 0), 1) + yield _obs( + total / count / 1e9, + {"modelName": model or "unknown", "spanKind": kind}, + ) + + def _error_cb(options): + for (model, kind), count in c.calls_error_count.items(): + yield _obs( + count, + {"modelName": model or "unknown", "spanKind": kind}, + ) + + def _slow_cb(options): + for (model, kind), count in c.calls_slow_count.items(): + yield _obs( + count, + {"modelName": model or "unknown", "spanKind": kind}, + ) + + def _ttft_cb(options): + for (model, kind), total in c.llm_first_token_ns_sum.items(): + count = max(c.llm_first_token_count.get((model, kind), 0), 1) + yield _obs( + total / count / 1e9, + {"modelName": model or "unknown", "spanKind": kind}, + ) + + def _tokens_input_cb(options): + for (model, kind), total in c.llm_usage_input_tokens.items(): + yield _obs( + total, + { + "modelName": model or "unknown", + "spanKind": kind, + "usageType": "input", + }, + ) + + def _tokens_output_cb(options): + for (model, kind), total in c.llm_usage_output_tokens.items(): + yield _obs( + total, + { + "modelName": model or "unknown", + "spanKind": kind, + "usageType": "output", + }, + ) + + self._gauges.append( + self._meter.create_observable_gauge( + name="genai_calls_count", + callbacks=[_calls_cb], + description="Number of GenAI calls.", + ) + ) + self._gauges.append( + self._meter.create_observable_gauge( + name="genai_calls_duration_seconds", + callbacks=[_duration_cb], + description="Average GenAI call duration in seconds.", + unit="s", + ) + ) + self._gauges.append( + self._meter.create_observable_gauge( + name="genai_calls_error_count", + callbacks=[_error_cb], + description="Number of failed GenAI calls.", + ) + ) + self._gauges.append( + self._meter.create_observable_gauge( + name="genai_calls_slow_count", + callbacks=[_slow_cb], + description="Number of slow GenAI calls (threshold-configurable).", + ) + ) + self._gauges.append( + self._meter.create_observable_gauge( + name="genai_llm_first_token_seconds", + callbacks=[_ttft_cb], + description="Average LLM time-to-first-token in seconds.", + unit="s", + ) + ) + self._gauges.append( + self._meter.create_observable_gauge( + name="genai_llm_usage_tokens", + callbacks=[_tokens_input_cb, _tokens_output_cb], + description="LLM token usage (input / output).", + unit="{token}", + ) + ) + + # ---- SpanProcessor interface ---- + def on_start( + self, span: OtelSpan, parent_context: Optional[Context] = None + ) -> None: + try: + ctx = span.get_span_context() + sid = ctx.span_id + # str() hex form, used as dict key + key = format(sid, "016x") + except Exception: + return + self._live_spans[key] = span + parent = getattr(span, "_parent", None) + parent_id = None + if parent is not None: + try: + parent_id = format(parent.span_id, "016x") + except Exception: + parent_id = None + self._span_parents[key] = parent_id + + def on_end(self, span: Any) -> None: + """Enrich a just-ended MAF span with ARMS GenAI semantic conventions.""" + try: + ctx = span.get_span_context() + key = format(ctx.span_id, "016x") + except Exception: + return + live = self._live_spans.pop(key, None) + parent_id = self._span_parents.pop(key, None) + if live is None: + return + # NOTE: by the time on_end runs, the SDK has already called Span.end(), + # so is_recording() is False and the public set_attribute / set_status + # are no-ops. We mutate ``_attributes`` / ``_status`` directly (see + # ``_set_attr``). Same approach as the OpenInference processor. + + try: + name = span.name or "" + existing_op = _attr_value(span, GEN_AI_OPERATION_NAME) + existing_op = existing_op if isinstance(existing_op, str) else None + span_kind, op_name = _classify_span(name, existing_op, span) + + # 1) gen_ai.span.kind (only set if not already present) + if not _attr_value(span, GEN_AI_SPAN_KIND): + _set_attr(live, GEN_AI_SPAN_KIND, span_kind) + + # 2) gen_ai.operation.name (set if missing or freshly derived for + # workflow spans where MAF does not write it) + if not existing_op: + _set_attr(live, GEN_AI_OPERATION_NAME, op_name) + elif existing_op != op_name and span_kind in { + GenAISpanKind.TASK, + GenAISpanKind.AGENT, + }: + # executor.process reclassification (FunctionExecutor -> TASK, + # AgentExecutor -> AGENT/invoke_agent) + _set_attr(live, GEN_AI_OPERATION_NAME, op_name) + + # 3) gen_ai.framework (always — ARMS extension) + if not _attr_value(span, GEN_AI_FRAMEWORK): + _set_attr(live, GEN_AI_FRAMEWORK, FRAMEWORK_NAME) + + # 4) Rename MAF private-prefix attributes + _rename_maf_attrs(live, span) + + # 5) Normalize provider.name + provider = _attr_value(span, GEN_AI_PROVIDER_NAME) + normalized = _normalize_provider(provider) + if normalized is not None and normalized != provider: + _set_attr(live, GEN_AI_PROVIDER_NAME, normalized) + + # 6) TTFT backfill for LLM spans with streaming events + if span_kind == GenAISpanKind.LLM: + ttft = _ttft_from_events(span) + if ttft is not None and not _attr_value( + span, GEN_AI_RESPONSE_TTFT + ): + _set_attr(live, GEN_AI_RESPONSE_TTFT, ttft) + _set_attr(live, GEN_AI_USER_TTFT, ttft) + + # 7) ENTRY detection: a root invoke_agent span with no parent becomes + # the trace entry point. + if ( + span_kind == GenAISpanKind.AGENT + and op_name == GenAIOperation.INVOKE_AGENT + and parent_id is None + ): + # Only reclassify if there's no explicit ENTRY span above us + # (we cannot see siblings; this is best-effort). We keep AGENT + # kind on the actual agent span — ENTRY is represented by the + # AGENT span itself being the root, in line with the spec note + # that ENTRY is an entry-point identifier, not a separate kind + # unless an application-level ENTRY span exists. + pass + + # 8) Status: set OK on success (MAF only sets ERROR). The SDK + # passes a ``ReadableSpan`` snapshot to ``on_end``; its + # ``_status`` is a separate field from the live Span's, so we + # mutate the ReadableSpan's ``_status`` directly (and the live + # Span's too, for symmetry). The shared ``_attributes`` dict is + # mutated via ``_set_attr`` above and is visible to exporters. + current_status = getattr(span, "status", None) + status_code = getattr(current_status, "status_code", None) + if status_code != StatusCode.ERROR: + ok_status = Status(StatusCode.OK) + try: + span._status = ok_status # type: ignore[attr-defined] + except Exception as exc: # pragma: no cover - defensive + logger.debug("set_status OK on readable failed: %s", exc) + try: + live._status = ok_status # type: ignore[attr-defined] + except Exception: + pass + + # 9) error.type already set by MAF via capture_exception; nothing to do. + + # 10) Aggregate metrics + if self._metrics_enabled: + self._aggregate_metrics(span, span_kind, op_name) + + except Exception as exc: # pragma: no cover - defensive + logger.warning("MAFSemanticProcessor.on_end failed: %s", exc) + finally: + # Span has already been ended by the SDK; nothing to do here. + pass + + def _aggregate_metrics( + self, readable: Any, span_kind: str, op_name: str + ) -> None: + try: + model = _attr_value(readable, GEN_AI_REQUEST_MODEL) + if not model: + model = _attr_value(readable, GEN_AI_RESPONSE_MODEL) + model = model if isinstance(model, str) else "unknown" + key = (model, span_kind) + self._counters.calls_count[key] += 1 + + start = getattr(readable, "start_time", None) + end = getattr(readable, "end_time", None) + if start is not None and end is not None: + try: + duration_ns = int(end - start) + self._counters.calls_duration_ns_sum[key] += duration_ns + if duration_ns >= self._slow_threshold_ns: + self._counters.calls_slow_count[key] += 1 + except (TypeError, ValueError): + pass + + current_status = getattr(readable, "status", None) + status_code = getattr(current_status, "status_code", None) + if status_code == StatusCode.ERROR: + self._counters.calls_error_count[key] += 1 + elif _attr_value(readable, ERROR_TYPE): + self._counters.calls_error_count[key] += 1 + + if span_kind == GenAISpanKind.LLM: + ttft = _attr_value(readable, GEN_AI_RESPONSE_TTFT) + if isinstance(ttft, (int, float)) and ttft > 0: + self._counters.llm_first_token_ns_sum[key] += int(ttft) + self._counters.llm_first_token_count[key] += 1 + input_tokens = _attr_value(readable, GEN_AI_USAGE_INPUT_TOKENS) + if isinstance(input_tokens, (int, float)): + self._counters.llm_usage_input_tokens[key] += int(input_tokens) + output_tokens = _attr_value(readable, GEN_AI_USAGE_OUTPUT_TOKENS) + if isinstance(output_tokens, (int, float)): + self._counters.llm_usage_output_tokens[key] += int(output_tokens) + except Exception as exc: # pragma: no cover - defensive + logger.debug("MAF metrics aggregation failed: %s", exc) + + def shutdown(self) -> None: + self._live_spans.clear() + self._span_parents.clear() + + def force_flush(self, timeout_millis: int = 30000) -> bool: + return True + + +def _obs(value: float, attrs: Dict[str, str]): + """Build an Observation compatible with OTel callbacks.""" + from opentelemetry.metrics import Observation + + return Observation(value, attrs) + + +__all__ = ["MAFSemanticProcessor"] diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/version.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/version.py new file mode 100644 index 000000000..a6645841e --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/version.py @@ -0,0 +1 @@ +__version__ = "0.1.0.dev" diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/__init__.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_config.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_config.py new file mode 100644 index 000000000..166e0d77f --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_config.py @@ -0,0 +1,38 @@ +"""Tests for config env-var parsing.""" + +from __future__ import annotations + +import importlib + +from opentelemetry.instrumentation.microsoft_agent_framework import config + + +def _reload(): + importlib.reload(config) + return config + + +def test_defaults(monkeypatch): + monkeypatch.delenv("ARMS_MAF_INSTRUMENTATION_ENABLED", raising=False) + monkeypatch.delenv("ARMS_MAF_SENSITIVE_DATA_ENABLED", raising=False) + monkeypatch.delenv("ARMS_MAF_REACT_STEP_ENABLED", raising=False) + monkeypatch.delenv("ARMS_MAF_SLOW_THRESHOLD_MS", raising=False) + monkeypatch.delenv("ARMS_MAF_METRICS_ENABLED", raising=False) + cfg = _reload() + assert cfg.is_instrumentation_enabled() is True + assert cfg.is_sensitive_data_enabled() is False + assert cfg.is_react_step_enabled() is False + assert cfg.is_metrics_enabled() is True + assert cfg.get_slow_threshold_ms() == 1000 + + +def test_explicit_values(monkeypatch): + monkeypatch.setenv("ARMS_MAF_SENSITIVE_DATA_ENABLED", "true") + monkeypatch.setenv("ARMS_MAF_REACT_STEP_ENABLED", "1") + monkeypatch.setenv("ARMS_MAF_SLOW_THRESHOLD_MS", "2500") + monkeypatch.setenv("ARMS_MAF_METRICS_ENABLED", "off") + cfg = _reload() + assert cfg.is_sensitive_data_enabled() is True + assert cfg.is_react_step_enabled() is True + assert cfg.get_slow_threshold_ms() == 2500 + assert cfg.is_metrics_enabled() is False diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_processor.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_processor.py new file mode 100644 index 000000000..f443dd006 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_processor.py @@ -0,0 +1,311 @@ +"""Unit tests for MAFSemanticProcessor span enrichment. + +Tests verify that the processor correctly: +- Injects ``gen_ai.span.kind`` for each MAF span type. +- Renames MAF private-prefix attributes (``workflow.id`` → ``gen_ai.workflow.id`` …). +- Reclassifies ``executor.process`` spans by ``executor.type``. +- Normalizes ``gen_ai.provider.name``. +- Backfills ``gen_ai.response.time_to_first_token`` from streaming events. +- Sets ``StatusCode.OK`` on success, preserves ``ERROR`` on failure. +- Aggregates metrics counters. +""" + +from __future__ import annotations + +import time + +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) +from opentelemetry.trace import Status, StatusCode + +from opentelemetry.instrumentation.microsoft_agent_framework.span_processor import ( + MAFSemanticProcessor, +) +from opentelemetry.instrumentation.microsoft_agent_framework.semantic_conventions import ( + GEN_AI_FRAMEWORK, + GEN_AI_OPERATION_NAME, + GEN_AI_PROVIDER_NAME, + GEN_AI_RESPONSE_TTFT, + GEN_AI_SPAN_KIND, + GEN_AI_USAGE_INPUT_TOKENS, + GEN_AI_USAGE_OUTPUT_TOKENS, + GEN_AI_USER_TTFT, + GenAIOperation, + GenAISpanKind, +) + + +def _setup(): + """Return ``(tracer_provider, tracer, exporter)`` with the MAF processor.""" + tp = TracerProvider() + exporter = InMemorySpanExporter() + processor = MAFSemanticProcessor( + meter_provider=None, metrics_enabled=False, capture_sensitive_data=False + ) + tp.add_span_processor(processor) + tp.add_span_processor(SimpleSpanProcessor(exporter)) + tracer = tp.get_tracer("test") + return tp, tracer, exporter, processor + + +def _flush(exporter): + # Force spans to be exported to the in-memory exporter. + return exporter.get_finished_spans() + + +def test_llm_span_gets_llm_kind_and_chat_operation(): + tp, tracer, exporter, _ = _setup() + with tracer.start_as_current_span("chat gpt-4o") as span: + span.set_attribute(GEN_AI_OPERATION_NAME, GenAIOperation.CHAT) + span.set_attribute(GEN_AI_PROVIDER_NAME, "openai") + span.set_attribute("gen_ai.request.model", "gpt-4o") + span.set_attribute(GEN_AI_USAGE_INPUT_TOKENS, 10) + span.set_attribute(GEN_AI_USAGE_OUTPUT_TOKENS, 20) + spans = _flush(exporter) + assert len(spans) == 1 + s = spans[0] + assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.LLM + assert s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.CHAT + assert s.attributes.get(GEN_AI_FRAMEWORK) == "microsoft-agent-framework" + assert s.status.status_code == StatusCode.OK + + +def test_tool_span_gets_tool_kind(): + tp, tracer, exporter, _ = _setup() + with tracer.start_as_current_span("execute_tool get_weather") as span: + span.set_attribute(GEN_AI_OPERATION_NAME, GenAIOperation.EXECUTE_TOOL) + span.set_attribute("gen_ai.tool.name", "get_weather") + spans = _flush(exporter) + assert spans[0].attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.TOOL + + +def test_embedding_span(): + tp, tracer, exporter, _ = _setup() + with tracer.start_as_current_span("embeddings text-embedding-3-small") as span: + span.set_attribute(GEN_AI_OPERATION_NAME, GenAIOperation.EMBEDDINGS) + span.set_attribute("gen_ai.request.model", "text-embedding-3-small") + spans = _flush(exporter) + assert spans[0].attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.EMBEDDING + + +def test_agent_span(): + tp, tracer, exporter, _ = _setup() + with tracer.start_as_current_span("invoke_agent my-agent") as span: + span.set_attribute(GEN_AI_OPERATION_NAME, GenAIOperation.INVOKE_AGENT) + span.set_attribute("gen_ai.agent.name", "my-agent") + spans = _flush(exporter) + assert spans[0].attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.AGENT + + +def test_workflow_run_span_gets_chain_kind_and_workflow_op(): + tp, tracer, exporter, _ = _setup() + with tracer.start_as_current_span("workflow.run abc-123") as span: + span.set_attribute("workflow.id", "abc-123") + span.set_attribute("workflow.name", "MyWorkflow") + span.set_attribute("workflow.description", "d") + spans = _flush(exporter) + s = spans[0] + assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.CHAIN + assert s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.WORKFLOW + # MAF private-prefix renamed to gen_ai.* canonical keys + assert s.attributes.get("gen_ai.workflow.id") == "abc-123" + assert s.attributes.get("gen_ai.workflow.name") == "MyWorkflow" + # The original MAF private key should be removed (best-effort). + assert "workflow.id" not in s.attributes + + +def test_executor_process_function_executor_becomes_task(): + tp, tracer, exporter, _ = _setup() + with tracer.start_as_current_span("executor.process fid-1") as span: + # MAF does NOT set gen_ai.operation.name for executor.process spans. + span.set_attribute("executor.id", "fid-1") + span.set_attribute("executor.type", "FunctionExecutor") + spans = _flush(exporter) + s = spans[0] + assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.TASK + assert s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.TASK + assert s.attributes.get("gen_ai.task.name") == "fid-1" + assert s.attributes.get("gen_ai.task.type") == "FunctionExecutor" + + +def test_executor_process_agent_executor_becomes_agent(): + tp, tracer, exporter, _ = _setup() + with tracer.start_as_current_span("executor.process aid-1") as span: + span.set_attribute("executor.id", "aid-1") + span.set_attribute("executor.type", "AgentExecutor") + spans = _flush(exporter) + s = spans[0] + assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.AGENT + assert s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.INVOKE_AGENT + + +def test_executor_process_unknown_executor_stays_chain(): + tp, tracer, exporter, _ = _setup() + with tracer.start_as_current_span("executor.process xid") as span: + span.set_attribute("executor.id", "xid") + span.set_attribute("executor.type", "SomeOtherExecutor") + spans = _flush(exporter) + s = spans[0] + assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.CHAIN + assert s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.WORKFLOW + + +def test_message_send_span(): + tp, tracer, exporter, _ = _setup() + with tracer.start_as_current_span("message.send m-1") as span: + span.set_attribute("message.source_id", "src") + span.set_attribute("message.target_id", "tgt") + spans = _flush(exporter) + s = spans[0] + assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.CHAIN + assert s.attributes.get("gen_ai.message.source_id") == "src" + assert s.attributes.get("gen_ai.message.target_id") == "tgt" + + +def test_provider_normalization_azure_openai_to_openai(): + tp, tracer, exporter, _ = _setup() + with tracer.start_as_current_span("chat gpt-4o") as span: + span.set_attribute(GEN_AI_OPERATION_NAME, GenAIOperation.CHAT) + span.set_attribute(GEN_AI_PROVIDER_NAME, "azure_openai") + spans = _flush(exporter) + assert spans[0].attributes.get(GEN_AI_PROVIDER_NAME) == "openai" + + +def test_ttft_backfill_from_first_event(): + tp, tracer, exporter, _ = _setup() + with tracer.start_as_current_span("chat gpt-4o") as span: + span.set_attribute(GEN_AI_OPERATION_NAME, GenAIOperation.CHAT) + span.set_attribute(GEN_AI_PROVIDER_NAME, "openai") + # Emit a streaming-chunk event a bit after start. + # The SDK uses wall-clock ns for event timestamps. + # We just need the event to be present; the processor will compute the + # delta from start_time. + time.sleep(0.01) + span.add_event("streaming.chunk") + spans = _flush(exporter) + s = spans[0] + ttft = s.attributes.get(GEN_AI_RESPONSE_TTFT) + user_ttft = s.attributes.get(GEN_AI_USER_TTFT) + assert ttft is not None and ttft > 0 + assert user_ttft == ttft + + +def _setup_with_metrics(): + """Like ``_setup`` but with metrics aggregation enabled.""" + tp = TracerProvider() + exporter = InMemorySpanExporter() + processor = MAFSemanticProcessor( + meter_provider=None, metrics_enabled=True, capture_sensitive_data=False + ) + tp.add_span_processor(processor) + tp.add_span_processor(SimpleSpanProcessor(exporter)) + tracer = tp.get_tracer("test") + return tp, tracer, exporter, processor + + +def test_error_span_keeps_error_status_and_increments_error_counter(): + tp, tracer, exporter, processor = _setup_with_metrics() + try: + with tracer.start_as_current_span("chat gpt-4o") as span: + span.set_attribute(GEN_AI_OPERATION_NAME, GenAIOperation.CHAT) + span.set_attribute(GEN_AI_PROVIDER_NAME, "openai") + span.set_status(Status(StatusCode.ERROR, "boom")) + span.set_attribute("error.type", "RateLimitError") + raise RuntimeError("boom") + except RuntimeError: + pass + spans = _flush(exporter) + assert spans[0].status.status_code == StatusCode.ERROR + # Metric error counter incremented (model not set -> "unknown"). + found = False + for (m, k), count in processor._counters.calls_error_count.items(): + if k == GenAISpanKind.LLM and count == 1: + found = True + break + assert found, "error counter not incremented" + + +def test_metrics_counters_incremented_on_llm_span(): + tp, tracer, exporter, processor = _setup_with_metrics() + with tracer.start_as_current_span("chat gpt-4o") as span: + span.set_attribute(GEN_AI_OPERATION_NAME, GenAIOperation.CHAT) + span.set_attribute("gen_ai.request.model", "gpt-4o") + span.set_attribute(GEN_AI_USAGE_INPUT_TOKENS, 5) + span.set_attribute(GEN_AI_USAGE_OUTPUT_TOKENS, 7) + _ = _flush(exporter) + assert processor._counters.calls_count[("gpt-4o", GenAISpanKind.LLM)] == 1 + assert processor._counters.llm_usage_input_tokens[("gpt-4o", GenAISpanKind.LLM)] == 5 + assert processor._counters.llm_usage_output_tokens[("gpt-4o", GenAISpanKind.LLM)] == 7 + + +def test_react_step_span_classification(): + tp, tracer, exporter, _ = _setup() + with tracer.start_as_current_span("react step") as span: + # When emitted by our react_step_patch, the handler sets + # gen_ai.operation.name=react and gen_ai.span.kind=STEP itself. But + # if the processor sees a "react step" name without op set, it should + # classify it as STEP/react as a fallback. + pass + spans = _flush(exporter) + s = spans[0] + assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.STEP + assert s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.REACT + + +def test_uninstrument_releases_processor(): + from opentelemetry.instrumentation.microsoft_agent_framework import ( + MicrosoftAgentFrameworkInstrumentor, + ) + + # Without MAF installed we just exercise that _uninstrument does not raise + # and clears state. We construct the instrumentor and call _uninstrument + # without _instrument to ensure idempotent teardown. + inst = MicrosoftAgentFrameworkInstrumentor() + inst._uninstrument() + assert inst._processor is None + assert inst._react_applied is False + + +def test_maf_dict_attribute_is_serialized_via_gen_ai_json_dumps(): + """Dict/list attribute values written into ``_attributes`` by MAF under + private prefixes must be JSON-serialized via + ``opentelemetry.util.genai.utils.gen_ai_json_dumps`` when renamed to + ``gen_ai.*`` keys, because OTel SDKs reject arbitrary dict attribute + values. Hard constraint #2. + + We exercise this directly through ``_set_attr`` because the SDK drops + dict values at ``set_attribute`` time (logged as a warning), which is + exactly the scenario our coercion defends against when MAF mutates + ``_attributes`` directly itself (it does so in several internal paths). + """ + from opentelemetry.instrumentation.microsoft_agent_framework import ( + span_processor as sp, + ) + + tp, tracer, exporter, _ = _setup() + workflow_def = {"nodes": ["a", "b"], "edges": [{"from": "a", "to": "b"}]} + + # Drive the rename path through _set_attr (the same path on_end uses after + # the SDK has stopped accepting set_attribute calls). + with tracer.start_as_current_span("workflow.run xyz") as span: + sp._set_attr(span, "gen_ai.workflow.definition", workflow_def) + spans = _flush(exporter) + s = spans[0] + val = s.attributes.get("gen_ai.workflow.definition") + assert isinstance(val, str) + assert "nodes" in val and "edges" in val + + +def test_safe_dumps_uses_gen_ai_json_dumps(): + """Unit test the helper directly.""" + from opentelemetry.instrumentation.microsoft_agent_framework import ( + span_processor as sp, + ) + + # gen_ai_json_dumps round-trips standard JSON; our wrapper must preserve it. + out = sp._safe_dumps({"a": 1, "b": [1, 2]}) + assert isinstance(out, str) + assert "a" in out and "b" in out diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_react_step.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_react_step.py new file mode 100644 index 000000000..f3d6c57ea --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_react_step.py @@ -0,0 +1,70 @@ +"""Tests for react_step_patch module. + +These tests do not require ``agent-framework-core`` to be installed. They +verify: +- The module imports cleanly without MAF. +- ``apply_react_step_patch`` is a no-op (with a warning) when MAF internals + are missing. +- ``revert_react_step_patch`` is safe to call when not applied. +- The ``ExtendedTelemetryHandler.react_step`` context manager produces a span + with ``gen_ai.span.kind=STEP`` and ``gen_ai.operation.name=react`` when + invoked directly (the same path the patch uses). +""" + +from __future__ import annotations + +import logging + +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) +from opentelemetry.util.genai.extended_handler import get_extended_telemetry_handler +from opentelemetry.util.genai.extended_types import ReactStepInvocation + +from opentelemetry.instrumentation.microsoft_agent_framework import react_step_patch + + +def test_module_imports_without_maf(): + # Importing the module should not raise even though MAF is absent. + assert hasattr(react_step_patch, "apply_react_step_patch") + assert hasattr(react_step_patch, "revert_react_step_patch") + + +def test_apply_is_noop_when_maf_missing(caplog): + # MAF is not installed in this test env, so apply should warn and return. + react_step_patch.revert_react_step_patch() # ensure clean state + with caplog.at_level(logging.WARNING): + react_step_patch.apply_react_step_patch(tracer_provider=None) + assert react_step_patch._applied is False + assert any("MAF internals not found" in r.message for r in caplog.records) + + +def test_revert_is_safe_when_not_applied(): + react_step_patch.revert_react_step_patch() + # Should not raise and should leave state clean. + assert react_step_patch._applied is False + + +def test_handler_react_step_emits_step_span(): + """Directly exercise ``handler.react_step`` to confirm the span shape the + patch relies on: name ``react step``, ``gen_ai.span.kind=STEP``, + ``gen_ai.operation.name=react``, ``gen_ai.react.round`` propagated.""" + tp = TracerProvider() + exporter = InMemorySpanExporter() + tp.add_span_processor(SimpleSpanProcessor(exporter)) + handler = get_extended_telemetry_handler(tracer_provider=tp) + + step_inv = ReactStepInvocation(round=3) + with handler.react_step(step_inv) as step: + step.finish_reason = "stop" + + spans = exporter.get_finished_spans() + assert len(spans) == 1 + s = spans[0] + assert s.name == "react step" + assert s.attributes.get("gen_ai.span.kind") == "STEP" + assert s.attributes.get("gen_ai.operation.name") == "react" + assert s.attributes.get("gen_ai.react.round") == 3 + assert s.attributes.get("gen_ai.react.finish_reason") == "stop" From 418b4f6d106b4ae7bad8d0f902e2a2d41fdf15d2 Mon Sep 17 00:00:00 2001 From: "liuziming.lzm" Date: Wed, 24 Jun 2026 10:43:24 +0800 Subject: [PATCH 04/21] fix(microsoft-agent-framework): review [M1][M2][L1] [M1] MCP span classification: detect mcp.method.name attribute (or SpanKind.CLIENT + mcp.* fallback) in _classify_span and return (CLIENT, MCP). MAF_SPAN_NAME_PREFIXES documents that MCP is detected via attribute rather than prefix (method names are unbounded). [M2] revert_react_step_patch: capture originals BEFORE wrapping (via __wrapped__ unwrap chain), and switch from broken decorator form to wrap_function_wrapper(class, name, wrapper) + @wrapt.decorator. revert now restores the original; apply->revert->apply does not stack wrappers. [L1] _safe_dumps: cap output at 4096 chars (execute.md single-field cap) since gen_ai_json_dumps only serializes. Docstring + module docstring updated to match actual behavior. Tests: +6 (test_mcp_span_classified_as_client, mcp client-kind fallback, non-mcp client negative, _safe_dumps truncation, apply->revert->apply round-trip, _unwrap_to_function). 29 passed. --- .../react_step_patch.py | 53 +++++++- .../semantic_conventions.py | 7 ++ .../span_processor.py | 65 ++++++++-- .../tests/test_processor.py | 60 +++++++++ .../tests/test_react_step.py | 119 ++++++++++++++++++ 5 files changed, 291 insertions(+), 13 deletions(-) diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/react_step_patch.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/react_step_patch.py index f6cb50158..28990cb27 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/react_step_patch.py +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/react_step_patch.py @@ -76,9 +76,22 @@ def apply_react_step_patch(tracer_provider: Any = None) -> None: ) return + # Capture the unwrapped originals BEFORE installing the wrapt wrappers. + # ``wrap_function_wrapper`` replaces the class attribute in place, so + # capturing afterwards (as the previous version did) records the wrapper + # itself, making the legacy ``revert`` a no-op and stacking wrappers on + # apply→revert→apply. We also unwrap any pre-existing wrappers down to + # the underlying function so we only restore the layer we installed. + _original_fil_get_response = _unwrap_to_function( + FunctionInvocationLayer.get_response + ) + _original_chat_get_response = _unwrap_to_function( + ChatTelemetryLayer.get_response + ) + handler = _get_extended_handler(tracer_provider) - @wrapt.wrap_function_wrapper(FunctionInvocationLayer, "get_response") + @wrapt.decorator async def _fil_wrapper(wrapped, self, args, kwargs): # type: ignore[no-untyped-def] # Outer function is async per MAF's signature (overloads collapse to # one async implementation). Set the react-loop scope for the duration @@ -91,7 +104,7 @@ async def _fil_wrapper(wrapped, self, args, kwargs): # type: ignore[no-untyped- _maf_react_loop_active.reset(token_active) _maf_react_step_counter.reset(token_counter) - @wrapt.wrap_function_wrapper(ChatTelemetryLayer, "get_response") + @wrapt.decorator async def _chat_wrapper(wrapped, self, args, kwargs): # type: ignore[no-untyped-def] if not _maf_react_loop_active.get(): return await wrapped(*args, **kwargs) @@ -118,12 +131,44 @@ async def _chat_wrapper(wrapped, self, args, kwargs): # type: ignore[no-untyped finally: _maf_react_step_counter.reset(token_counter) - _original_fil_get_response = FunctionInvocationLayer.get_response - _original_chat_get_response = ChatTelemetryLayer.get_response + # ``wrap_function_wrapper`` takes (module_or_obj, name, wrapper). When + # given a class object + attribute name it patches the attribute on the + # class. Each call installs exactly one FunctionWrapper around the + # currently-bound attribute, so apply→revert→apply does not stack + # provided revert restores the original first. + wrapt.wrap_function_wrapper( + FunctionInvocationLayer, "get_response", _fil_wrapper + ) + wrapt.wrap_function_wrapper( + ChatTelemetryLayer, "get_response", _chat_wrapper + ) + _applied = True logger.info("MAF ReAct step patch applied (handler.react_step).") +def _unwrap_to_function(func: Any) -> Any: + """Return the underlying function, peeling any wrapt wrappers. + + ``wrapt.FunctionWrapper`` exposes the wrapped callable via ``__wrapped__``. + We walk that chain so that, when an unrelated instrumentor has already + wrapped the target, we restore only the layer we installed (not the + unrelated one — that is a separate concern). When the target is not a + wrapper, ``__wrapped__`` is absent and we return ``func`` unchanged. + """ + seen: set[int] = set() + cur = func + while True: + if id(cur) in seen: # defensive against cycles + break + seen.add(id(cur)) + nxt = getattr(cur, "__wrapped__", None) + if nxt is None or nxt is cur: + break + cur = nxt + return cur + + def _extract_finish_reason(result: Any) -> Any: """Best-effort extraction of a finish_reason string from a ChatResponse.""" try: diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py index f9c033f0f..cba22abfb 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py @@ -48,6 +48,13 @@ class GenAIOperation: # MAF span-name prefixes (from observability.py OtelAttr) — used to classify a # span when ``gen_ai.operation.name`` is not already set on it. +# +# Note: MCP spans are *not* listed here. Their names follow +# ``{mcp.method.name} {target}`` (e.g. ``tools/call get_weather``, +# ``initialize``) so the method name is unbounded and would collide with other +# prefixes. MCP is detected via the ``mcp.method.name`` attribute (set +# unconditionally by MAF's ``create_mcp_client_span`` at +# ``observability.py:2101``) in :func:`span_processor._is_mcp_span`. MAF_SPAN_NAME_PREFIXES: Final[dict[str, str]] = { "chat ": GenAIOperation.CHAT, "embeddings ": GenAIOperation.EMBEDDINGS, diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py index a9860cb0f..5f14d8be5 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py @@ -18,9 +18,11 @@ / ``genai_calls_error_count`` / ``genai_calls_slow_count`` / ``genai_llm_first_token_seconds`` / ``genai_llm_usage_tokens``) in-process, exposed via observable gauges. -Truncation / PII helpers are reused from ``opentelemetry.util.genai.utils`` +Truncation / JSON serialization are reused from ``opentelemetry.util.genai.utils`` (``gen_ai_json_dumps``) — aligned with the pattern at ``instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/.../span_processor.py:27``. +``gen_ai_json_dumps`` itself only serializes (it does not truncate), so the +single-field 4 KB cap from execute.md is enforced in :func:`_safe_dumps`. """ from __future__ import annotations @@ -90,17 +92,23 @@ def _attr_value(span: Any, key: str) -> Any: def _safe_dumps(obj: Any) -> Optional[str]: - """Serialize ``obj`` via the shared ``gen_ai_json_dumps`` helper. + """Serialize ``obj`` to a JSON string capped at 4 KB. - Reuses the truncation / PII / canonicalization logic from + Uses the shared ``gen_ai_json_dumps`` helper from ``opentelemetry.util.genai.utils`` (the same path as - ``openai-agents-v2/span_processor.py:27`` / ``safe_json_dumps`` at - ``:370``). Falls back to ``str(obj)`` if JSON serialization fails. + ``openai-agents-v2/span_processor.py:27``) for compact, ASCII-preserving + JSON serialization of arbitrary objects (bytes / datetimes / nested + dicts). Note that ``gen_ai_json_dumps`` itself does *not* truncate — it is + just ``json.dumps`` with a custom encoder — so we cap the output at 4 KB + here to honour the execute.md single-field cap (per-attribute budget + shared with the rename path). Falls back to ``str(obj)`` if JSON + serialization fails. """ try: - return gen_ai_json_dumps(obj) + out = gen_ai_json_dumps(obj) except (TypeError, ValueError): - return str(obj) + out = str(obj) + return out[:4096] _PRIMITIVE_ATTR_TYPES = (str, bool, int, float) @@ -214,6 +222,31 @@ def _normalize_provider(value: Any) -> Optional[str]: return PROVIDER_NAME_NORMALIZE.get(value, value) +_MCP_METHOD_NAME_ATTR = "mcp.method.name" + + +def _is_mcp_span(readable: Any) -> bool: + """Return True if ``readable`` is an MCP client span. + + MAF's ``create_mcp_client_span`` (``observability.py:2101``) always sets + ``mcp.method.name``. We also accept ``SpanKind.CLIENT`` + any ``mcp.*`` + attribute as a fallback in case MAF later renames the attribute. + """ + if _attr_value(readable, _MCP_METHOD_NAME_ATTR) is not None: + return True + try: + from opentelemetry.trace import SpanKind + + kind = getattr(readable, "kind", None) + if kind == SpanKind.CLIENT: + attrs = getattr(readable, "attributes", None) or {} + if any(str(k).startswith("mcp.") for k in attrs): + return True + except Exception: + pass + return False + + def _classify_span( name: str, operation: Optional[str], readable: Any ) -> Tuple[str, str]: @@ -221,9 +254,23 @@ def _classify_span( Classification priority: 1. Existing ``gen_ai.operation.name`` (set by MAF for chat/embeddings/tool/agent). - 2. Span-name prefix matching (workflow spans have no operation.name from MAF). - 3. ``react step`` literal name (emitted by our react_step patch). + 2. MCP attribute detection — MAF's ``create_mcp_client_span`` + (``observability.py:2083``) emits spans named ``{mcp.method.name} {target}`` + with no ``gen_ai.operation.name`` set; the ``mcp.method.name`` attribute + (always present) is the reliable signal. Falls back to + ``SpanKind.CLIENT`` + any ``mcp.*`` attribute. MCP is intentionally + *not* in ``MAF_SPAN_NAME_PREFIXES`` because its method names are + unbounded (``initialize``, ``tools/call`` …) and would collide with + other prefixes. + 3. Span-name prefix matching (workflow spans have no operation.name from MAF). + 4. ``react step`` literal name (emitted by our react_step patch). """ + # MCP detection — runs before operation-based matching because MAF does not + # write ``gen_ai.operation.name`` for MCP spans. We check the attribute + # directly (cheap; happens once per span on_end). + if _is_mcp_span(readable): + return GenAISpanKind.CLIENT, GenAIOperation.MCP + if operation: op = operation else: diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_processor.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_processor.py index f443dd006..2f85a6c2a 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_processor.py +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_processor.py @@ -309,3 +309,63 @@ def test_safe_dumps_uses_gen_ai_json_dumps(): out = sp._safe_dumps({"a": 1, "b": [1, 2]}) assert isinstance(out, str) assert "a" in out and "b" in out + + +def test_safe_dumps_truncates_at_4kb(): + """_safe_dumps must cap output at 4096 chars (execute.md single-field cap).""" + from opentelemetry.instrumentation.microsoft_agent_framework import ( + span_processor as sp, + ) + + big = {"k": "x" * 10_000} + out = sp._safe_dumps(big) + assert isinstance(out, str) + assert len(out) <= 4096 + + +def test_mcp_span_classified_as_client(): + """MCP spans emitted by MAF's ``create_mcp_client_span`` carry no + ``gen_ai.operation.name``; their name is ``{mcp.method.name} {target}`` + (unbounded), so they must be detected via the ``mcp.method.name`` + attribute and classified as ``(CLIENT, mcp)``. Regression for [M1]. + """ + from opentelemetry.trace import SpanKind + + tp, tracer, exporter, _ = _setup() + with tracer.start_as_current_span( + "tools/call get_weather", kind=SpanKind.CLIENT + ) as span: + # MAF writes mcp.method.name (no gen_ai.operation.name). + span.set_attribute("mcp.method.name", "tools/call") + span.set_attribute("mcp.session.id", "sess-1") + spans = _flush(exporter) + s = spans[0] + assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.CLIENT + assert s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.MCP + + +def test_mcp_span_via_client_kind_and_mcp_attr_fallback(): + """Fallback path: a CLIENT span with any ``mcp.*`` attribute (but missing + ``mcp.method.name``) is still classified as MCP.""" + from opentelemetry.trace import SpanKind + + tp, tracer, exporter, _ = _setup() + with tracer.start_as_current_span("initialize", kind=SpanKind.CLIENT) as span: + span.set_attribute("mcp.protocol.version", "2024-11-05") + spans = _flush(exporter) + s = spans[0] + assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.CLIENT + assert s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.MCP + + +def test_non_mcp_client_span_is_not_misclassified_as_mcp(): + """A CLIENT span without any ``mcp.*`` attribute must NOT be classified as + MCP — guards against false positives on unrelated client spans.""" + from opentelemetry.trace import SpanKind + + tp, tracer, exporter, _ = _setup() + with tracer.start_as_current_span("http request", kind=SpanKind.CLIENT) as span: + span.set_attribute("http.method", "GET") + spans = _flush(exporter) + s = spans[0] + assert s.attributes.get(GEN_AI_SPAN_KIND) != GenAISpanKind.CLIENT diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_react_step.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_react_step.py index f3d6c57ea..0563629b8 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_react_step.py +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_react_step.py @@ -68,3 +68,122 @@ def test_handler_react_step_emits_step_span(): assert s.attributes.get("gen_ai.operation.name") == "react" assert s.attributes.get("gen_ai.react.round") == 3 assert s.attributes.get("gen_ai.react.finish_reason") == "stop" + + +def _install_fake_maf_modules(monkeypatch): + """Install minimal fake ``agent_framework._tools`` and ``agent_framework. + observability`` modules into ``sys.modules`` so the patch can wrap + classes without the real MAF package installed. + + Returns ``(fil_cls, chat_cls)`` — the two fake classes with their original + ``get_response`` callables recorded. + """ + import asyncio + import sys + import types + + async def _fil_get_response(self, *args, **kwargs): # pragma: no cover + return "fil" + + async def _chat_get_response(self, *args, **kwargs): # pragma: no cover + return "chat" + + class _FunctionInvocationLayer: + get_response = staticmethod(_fil_get_response) + + class _ChatTelemetryLayer: + get_response = staticmethod(_chat_get_response) + + tools_mod = types.ModuleType("agent_framework._tools") + tools_mod.FunctionInvocationLayer = _FunctionInvocationLayer # type: ignore[attr-defined] + obs_mod = types.ModuleType("agent_framework.observability") + obs_mod.ChatTelemetryLayer = _ChatTelemetryLayer # type: ignore[attr-defined] + af_mod = types.ModuleType("agent_framework") + af_mod._tools = tools_mod # type: ignore[attr-defined] + af_mod.observability = obs_mod # type: ignore[attr-defined] + + monkeypatch.setitem(sys.modules, "agent_framework", af_mod) + monkeypatch.setitem(sys.modules, "agent_framework._tools", tools_mod) + monkeypatch.setitem( + sys.modules, "agent_framework.observability", obs_mod + ) + return _FunctionInvocationLayer, _ChatTelemetryLayer + + +def test_react_patch_apply_revert_apply_no_multi_wrap(monkeypatch): + """[M2] regression: ``apply → revert → apply`` must not stack wrappers. + + Before the fix, ``_original_*`` was captured *after* the wrapt decorator + ran, so it stored the wrapper itself; ``revert`` was a no-op and a second + ``apply`` wrapped the (still-wrapped) function again, producing nested + wrappers. With the fix the original is captured before wrapping (via + ``__wrapped__`` unwrapping), so revert truly restores it and re-apply + produces a single layer. + """ + fil_cls, chat_cls = _install_fake_maf_modules(monkeypatch) + + # Reset module state (other tests may have left it set). + react_step_patch.revert_react_step_patch() + react_step_patch._applied = False + react_step_patch._original_fil_get_response = None + react_step_patch._original_chat_get_response = None + react_step_patch._handler = None + + fil_before = fil_cls.get_response + chat_before = chat_cls.get_response + + # 1) apply + react_step_patch.apply_react_step_patch(tracer_provider=None) + assert react_step_patch._applied is True + fil_after_1 = fil_cls.get_response + chat_after_1 = chat_cls.get_response + assert fil_after_1 is not fil_before, "wrapt did not replace FIL.get_response" + assert chat_after_1 is not chat_before, "wrapt did not replace Chat.get_response" + + # 2) revert + react_step_patch.revert_react_step_patch() + assert react_step_patch._applied is False + # After revert the attribute must point to the *original* (unwrapped) + # function — not to the wrapper. + assert fil_cls.get_response is fil_before, "revert did not restore FIL.get_response" + assert chat_cls.get_response is chat_before, "revert did not restore Chat.get_response" + + # 3) apply again + react_step_patch.apply_react_step_patch(tracer_provider=None) + assert react_step_patch._applied is True + fil_after_2 = fil_cls.get_response + chat_after_2 = chat_cls.get_response + # Same wrapper identity as the first apply would mean we did not stack; + # in any case, the underlying __wrapped__ must equal the original. + assert react_step_patch._unwrap_to_function(fil_after_2) is fil_before + assert react_step_patch._unwrap_to_function(chat_after_2) is chat_before + + # Depth check: walking __wrapped__ from the second wrapper must reach the + # original in a bounded number of steps (== 1, since revert restored it). + depth = 0 + cur = fil_after_2 + while getattr(cur, "__wrapped__", None) is not None and cur.__wrapped__ is not cur: + cur = cur.__wrapped__ + depth += 1 + assert depth < 8, "wrapper chain too deep — multi-wrap detected" + assert cur is fil_before + + react_step_patch.revert_react_step_patch() + + +def test_unwrap_to_function_peels_wrappers(monkeypatch): + """``_unwrap_to_function`` walks the ``__wrapped__`` chain to the + underlying callable, and returns non-wrappers unchanged.""" + fil_cls, _ = _install_fake_maf_modules(monkeypatch) + original = fil_cls.get_response + + assert react_step_patch._unwrap_to_function(original) is original + + # Build a fake wrapper chain + class _Wrapper: + def __init__(self, wrapped): + self.__wrapped__ = wrapped + + w1 = _Wrapper(original) + w2 = _Wrapper(w1) + assert react_step_patch._unwrap_to_function(w2) is original From 57d594e1f5228ae3eecf59acd079d2dd50aa0453 Mon Sep 17 00:00:00 2001 From: "liuziming.lzm" Date: Wed, 24 Jun 2026 13:49:23 +0800 Subject: [PATCH 05/21] fix(microsoft-agent-framework): validation P0/P1/P3/P5 - P0: react_step_patch wrappers now return coroutines from sync wrappers so MAF's await layer.get_response no longer raises TypeError. ContextVar tokens are set inside the coroutine body so set/reset share the same asyncio task context. - P1: extend op_name override condition to include CLIENT span kind so MCP tools/call inner spans get gen_ai.operation.name=mcp even when MAF pre-wrote execute_tool. - P3: provider.name normalization now handles sequence values (MAF emits list-wrapped values on AGENT spans) and falls back to case-insensitive matching so microsoft.agent_framework -> openai on AGENT spans. - P5: instrument() prepends MAFSemanticProcessor to the SDK processor tuple so on_end enrichments run before exporter processors registered earlier in bootstrap. Adds 7 unit tests; pytest tests/ -> 36 passed. --- .../microsoft_agent_framework/__init__.py | 37 ++++++ .../react_step_patch.py | 101 ++++++++++------ .../span_processor.py | 38 +++++- .../tests/test_processor.py | 113 ++++++++++++++++++ .../tests/test_react_step.py | 103 ++++++++++++++++ 5 files changed, 351 insertions(+), 41 deletions(-) diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/__init__.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/__init__.py index fda3ab282..2efd3aa0a 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/__init__.py +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/__init__.py @@ -103,6 +103,43 @@ def _instrument(self, **kwargs: Any) -> None: except Exception as exc: logger.warning("add_span_processor failed: %s", exc) raise + + # Ensure our processor runs FIRST in the pipeline so its ``on_end`` + # enrichments (gen_ai.span.kind / operation.name / framework / rename + # map / provider normalization) are visible to any exporter processors + # that were registered before us (e.g. by user bootstrap scripts that + # add ``ConsoleSpanExporter`` / ``OTLPSpanExporter`` before + # ``instrument()``). ``add_span_processor`` appends; processors are + # invoked in registration order on ``on_end``, so we move ourselves + # to index 0. The SDK stores the list as a tuple on + # ``_active_span_processor._span_processors`` (defensive — falls back + # to a list-style attribute on alternative provider layouts). + try: + asp = getattr(tracer_provider, "_active_span_processor", None) + span_processors = ( + getattr(asp, "_span_processors", None) + if asp is not None + else None + ) + if span_processors is None: + span_processors = getattr( + tracer_provider, "_span_processors", None + ) + # ``span_processors`` may be a tuple (current SDK) or a list. + if isinstance(span_processors, tuple): + others = tuple(p for p in span_processors if p is not processor) + new_procs = (processor,) + others + if asp is not None: + asp._span_processors = new_procs # type: ignore[attr-defined] + else: + tracer_provider._span_processors = new_procs # type: ignore[attr-defined] + elif isinstance(span_processors, list): + if processor in span_processors: + span_processors.remove(processor) + span_processors.insert(0, processor) + except Exception as exc: # pragma: no cover - defensive + logger.debug("prepend processor failed: %s", exc) + self._processor = processor # 3) Optional ReAct step patch (default OFF). diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/react_step_patch.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/react_step_patch.py index 28990cb27..652d66879 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/react_step_patch.py +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/react_step_patch.py @@ -91,45 +91,74 @@ def apply_react_step_patch(tracer_provider: Any = None) -> None: handler = _get_extended_handler(tracer_provider) - @wrapt.decorator - async def _fil_wrapper(wrapped, self, args, kwargs): # type: ignore[no-untyped-def] - # Outer function is async per MAF's signature (overloads collapse to - # one async implementation). Set the react-loop scope for the duration - # of the call. - token_active = _maf_react_loop_active.set(True) - token_counter = _maf_react_step_counter.set(0) - try: - return await wrapped(*args, **kwargs) - finally: - _maf_react_loop_active.reset(token_active) - _maf_react_step_counter.reset(token_counter) - - @wrapt.decorator - async def _chat_wrapper(wrapped, self, args, kwargs): # type: ignore[no-untyped-def] + # The wrappers are *synchronous* functions that return coroutines. We + # deliberately avoid ``@wrapt.decorator`` on ``async def`` here: + # ``wrapt.wrap_function_wrapper`` installs a ``FunctionWrapper`` whose + # ``__call__`` invokes our wrapper and returns whatever it returns. If our + # wrapper were ``async def``, the FunctionWrapper would still return a + # coroutine — *but* MAF 1.0.0's ``_call_chat_client`` is itself ``async def`` + # and is called as ``await layer.get_response(...)`` from ``_agents.py``. + # Empirically (validation report P0) the async-wrapped variant does NOT + # produce a coroutine on ``await`` — the call site raises ``TypeError``. + # Returning a coroutine from a sync wrapper is the simplest, robust fix: + # the caller's ``await`` resolves the coroutine normally. + # + # ContextVar tokens are set *inside* the coroutine body (not at wrapper + # entry) so set/reset share the same asyncio Task context. asyncio copies + # the context when a Task is created; tokens created outside the task + # cannot be reset inside it (``ValueError: created in a different + # Context``). Doing the ``set`` inside the coroutine guarantees the token + # belongs to whichever context ends up running the coroutine. + def _fil_wrapper(wrapped, instance, args, kwargs): # type: ignore[no-untyped-def] + async def _scoped(): # type: ignore[no-untyped-def] + token_active = _maf_react_loop_active.set(True) + token_counter = _maf_react_step_counter.set(0) + try: + return await wrapped(*args, **kwargs) + finally: + _maf_react_loop_active.reset(token_active) + _maf_react_step_counter.reset(token_counter) + + return _scoped() + + def _chat_wrapper(wrapped, instance, args, kwargs): # type: ignore[no-untyped-def] + # Read the loop-active flag synchronously (cheap; copied into the + # coroutine below). When False we pass the wrapped coroutine through + # unchanged — ``wrapped`` is a coroutine function so calling it returns + # a coroutine and the caller awaits it directly. if not _maf_react_loop_active.get(): - return await wrapped(*args, **kwargs) + return wrapped(*args, **kwargs) - # Each LLM call within the ReAct loop = one step. + # Pre-compute the round number for this call (read before the + # coroutine body so subsequent chat calls in the same loop see the + # incremented value at the same context level — same semantics as the + # prior implementation). round_num = _maf_react_step_counter.get() + 1 - token_counter = _maf_react_step_counter.set(round_num) - - from opentelemetry.util.genai.extended_types import ReactStepInvocation - - step_inv = ReactStepInvocation(round=round_num) - try: - with handler.react_step(step_inv) as step: - try: - result = await wrapped(*args, **kwargs) - # Best-effort finish_reason extraction from the response. - finish = _extract_finish_reason(result) - if finish is not None: - step.finish_reason = finish - return result - except Exception as exc: - step.finish_reason = "error" - raise - finally: - _maf_react_step_counter.reset(token_counter) + local_handler = handler + + async def _step_scoped(): # type: ignore[no-untyped-def] + from opentelemetry.util.genai.extended_types import ( + ReactStepInvocation, + ) + + step_inv = ReactStepInvocation(round=round_num) + token_counter = _maf_react_step_counter.set(round_num) + try: + with local_handler.react_step(step_inv) as step: + try: + result = await wrapped(*args, **kwargs) + # Best-effort finish_reason extraction from the response. + finish = _extract_finish_reason(result) + if finish is not None: + step.finish_reason = finish + return result + except Exception: + step.finish_reason = "error" + raise + finally: + _maf_react_step_counter.reset(token_counter) + + return _step_scoped() # ``wrap_function_wrapper`` takes (module_or_obj, name, wrapper). When # given a class object + attribute name it patches the attribute on the diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py index 5f14d8be5..924fad16b 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py @@ -215,11 +215,33 @@ def _rename_maf_attrs(live_span: OtelSpan, readable: Any) -> list[str]: def _normalize_provider(value: Any) -> Optional[str]: - if not value: + """Normalize ``gen_ai.provider.name`` to the ARMS canonical value. + + MAF writes a few variants (``azure_openai``, ``microsoft.agent_framework``, + different casing on AGENT vs LLM spans, or wraps the value in a sequence + for some span types). We: + + 1. Unwrap sequence attribute values (OTel allows ``str | sequence[str]``). + 2. Try an exact match against ``PROVIDER_NAME_NORMALIZE``. + 3. Fall back to a case-insensitive match — MAF emits + ``microsoft.agent_framework`` on AGENT spans in a slightly different + spelling than the LLM span's ``openai``, and we want both to collapse + to the same dimension regardless of casing. + """ + if value is None: return None + if isinstance(value, (list, tuple)): + if not value: + return None + value = value[0] if not isinstance(value, str): value = str(value) - return PROVIDER_NAME_NORMALIZE.get(value, value) + if value in PROVIDER_NAME_NORMALIZE: + return PROVIDER_NAME_NORMALIZE[value] + lowered = value.lower() + if lowered in PROVIDER_NAME_NORMALIZE: + return PROVIDER_NAME_NORMALIZE[lowered] + return value _MCP_METHOD_NAME_ATTR = "mcp.method.name" @@ -564,15 +586,21 @@ def on_end(self, span: Any) -> None: _set_attr(live, GEN_AI_SPAN_KIND, span_kind) # 2) gen_ai.operation.name (set if missing or freshly derived for - # workflow spans where MAF does not write it) + # workflow spans where MAF does not write it). For spans MAF + # mislabels (e.g. MCP ``tools/call`` written by MAF as + # ``execute_tool`` — see ``create_mcp_client_span`` at + # ``observability.py:2101``) we also override when our + # classification disagrees, provided the span is one of the + # kinds whose operation.name we own (TASK/AGENT reclassification + # of ``executor.process``, plus CLIENT for MCP — MAF writes the + # LLM's ``execute_tool`` value onto MCP inner spans). if not existing_op: _set_attr(live, GEN_AI_OPERATION_NAME, op_name) elif existing_op != op_name and span_kind in { GenAISpanKind.TASK, GenAISpanKind.AGENT, + GenAISpanKind.CLIENT, }: - # executor.process reclassification (FunctionExecutor -> TASK, - # AgentExecutor -> AGENT/invoke_agent) _set_attr(live, GEN_AI_OPERATION_NAME, op_name) # 3) gen_ai.framework (always — ARMS extension) diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_processor.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_processor.py index 2f85a6c2a..039c209c5 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_processor.py +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_processor.py @@ -369,3 +369,116 @@ def test_non_mcp_client_span_is_not_misclassified_as_mcp(): spans = _flush(exporter) s = spans[0] assert s.attributes.get(GEN_AI_SPAN_KIND) != GenAISpanKind.CLIENT + + +def test_mcp_span_op_name_overridden_to_mcp_when_maf_writes_execute_tool(): + """[P1] regression: MAF emits ``gen_ai.operation.name=execute_tool`` on the + MCP ``tools/call`` inner span (its ``create_mcp_client_span`` reuses the + tool-call op name even though it sets ``mcp.method.name``). The processor + must override the op name to ``mcp`` so the span is not mislabeled as a + TOOL call in the ARMS pipeline. + + Before the fix, ``on_end``'s op-name override only fired when + ``span_kind in {TASK, AGENT}`` — CLIENT (MCP) was missing, so the inner + span kept MAF's ``execute_tool`` value. + """ + from opentelemetry.trace import SpanKind + + tp, tracer, exporter, _ = _setup() + with tracer.start_as_current_span( + "tools/call slow_summary", kind=SpanKind.CLIENT + ) as span: + # MAF writes both mcp.method.name AND gen_ai.operation.name=execute_tool. + span.set_attribute("mcp.method.name", "tools/call") + span.set_attribute(GEN_AI_OPERATION_NAME, GenAIOperation.EXECUTE_TOOL) + spans = _flush(exporter) + s = spans[0] + assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.CLIENT + assert s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.MCP + + +def test_provider_normalization_microsoft_agent_framework_to_openai(): + """[P3] regression: AGENT spans written by MAF carry + ``gen_ai.provider.name=microsoft.agent_framework``. The + ``PROVIDER_NAME_NORMALIZE`` map must collapse this to ``openai`` so AGENT + spans share the same dimension as the LLM spans beneath them. Before the + fix, the AGENT span's provider stayed at the raw MAF value while LLM was + already ``openai``. + """ + tp, tracer, exporter, _ = _setup() + with tracer.start_as_current_span("invoke_agent my-agent") as span: + span.set_attribute(GEN_AI_OPERATION_NAME, GenAIOperation.INVOKE_AGENT) + span.set_attribute(GEN_AI_PROVIDER_NAME, "microsoft.agent_framework") + spans = _flush(exporter) + assert ( + spans[0].attributes.get(GEN_AI_PROVIDER_NAME) == "openai" + ), "AGENT provider.name should normalize from microsoft.agent_framework to openai" + + +def test_provider_normalization_case_insensitive_variant(): + """[P3] MAF may emit the provider value in different casing across span + types (AGENT vs LLM). Normalization should collapse to the same canonical + value regardless of case.""" + tp, tracer, exporter, _ = _setup() + with tracer.start_as_current_span("invoke_agent my-agent") as span: + span.set_attribute(GEN_AI_OPERATION_NAME, GenAIOperation.INVOKE_AGENT) + span.set_attribute(GEN_AI_PROVIDER_NAME, "Microsoft.Agent_Framework") + spans = _flush(exporter) + assert spans[0].attributes.get(GEN_AI_PROVIDER_NAME) == "openai" + + +def test_provider_normalization_list_wrapped_value(): + """[P3] OTel attributes may be a sequence of strings. MAF occasionally + writes ``gen_ai.provider.name`` as ``["microsoft.agent_framework"]`` on + AGENT spans. The normalizer should unwrap the sequence and collapse the + first element.""" + tp, tracer, exporter, _ = _setup() + with tracer.start_as_current_span("invoke_agent my-agent") as span: + span.set_attribute(GEN_AI_OPERATION_NAME, GenAIOperation.INVOKE_AGENT) + span.set_attribute(GEN_AI_PROVIDER_NAME, ["microsoft.agent_framework"]) + spans = _flush(exporter) + assert spans[0].attributes.get(GEN_AI_PROVIDER_NAME) == "openai" + + +def test_instrument_prepends_processor_before_existing_exporters(): + """[P5] When exporters were registered before ``instrument()`` (the common + bootstrap order: provider → exporter processor → instrument()), the MAF + semantic processor must run FIRST in the pipeline so its ``on_end`` + enrichments (gen_ai.span.kind, operation.name, framework, rename map, + provider normalization) are visible to those exporters. Without the + prepend, an exporter that captured the span before our ``on_end`` would + ship an un-enriched span. + """ + from opentelemetry.instrumentation.microsoft_agent_framework import ( + MicrosoftAgentFrameworkInstrumentor, + ) + + tp = TracerProvider() + exporter = InMemorySpanExporter() + # Bootstrap-style order: exporter processor FIRST, then our instrumentor. + tp.add_span_processor(SimpleSpanProcessor(exporter)) + + inst = MicrosoftAgentFrameworkInstrumentor() + # Skip MAF enable_instrumentation (MAF not installed in this env). + inst._instrument( + tracer_provider=tp, + react_step_enabled=False, + ) + try: + asp = getattr(tp, "_active_span_processor", None) + procs = ( + getattr(asp, "_span_processors", None) + if asp is not None + else getattr(tp, "_span_processors", None) + ) + assert procs is not None and len(procs) >= 2 + from opentelemetry.instrumentation.microsoft_agent_framework.span_processor import ( + MAFSemanticProcessor as _Proc, + ) + + assert isinstance(procs[0], _Proc), ( + "MAFSemanticProcessor must be at index 0 so it runs before " + "exporter processors" + ) + finally: + inst._uninstrument() diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_react_step.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_react_step.py index 0563629b8..e5e3f5141 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_react_step.py +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_react_step.py @@ -187,3 +187,106 @@ def __init__(self, wrapped): w1 = _Wrapper(original) w2 = _Wrapper(w1) assert react_step_patch._unwrap_to_function(w2) is original + + +def test_fil_wrapper_returns_coroutine(monkeypatch): + """[P0] regression: ``FunctionInvocationLayer.get_response`` wrapper must + return a coroutine when wrapping an ``async def`` function, so the MAF + runtime's ``await layer.get_response(...)`` (``_agents.py:964``) does not + raise ``TypeError``. The previous ``@wrapt.decorator`` + ``async def`` + variant produced a non-awaitable FunctionWrapper under MAF 1.0.0. + """ + import asyncio + import sys + import types + + async def _fil_get_response(self, *args, **kwargs): + return ("fil-ok", self, args, kwargs) + + class _FunctionInvocationLayer: + get_response = staticmethod(_fil_get_response) + + tools_mod = types.ModuleType("agent_framework._tools") + tools_mod.FunctionInvocationLayer = _FunctionInvocationLayer # type: ignore[attr-defined] + obs_mod = types.ModuleType("agent_framework.observability") + + class _ChatTelemetryLayer: + get_response = staticmethod(lambda *a, **kw: None) + + obs_mod.ChatTelemetryLayer = _ChatTelemetryLayer # type: ignore[attr-defined] + af_mod = types.ModuleType("agent_framework") + af_mod._tools = tools_mod # type: ignore[attr-defined] + af_mod.observability = obs_mod # type: ignore[attr-defined] + + monkeypatch.setitem(sys.modules, "agent_framework", af_mod) + monkeypatch.setitem(sys.modules, "agent_framework._tools", tools_mod) + monkeypatch.setitem(sys.modules, "agent_framework.observability", obs_mod) + + # Reset module state. + react_step_patch.revert_react_step_patch() + react_step_patch._applied = False + react_step_patch._original_fil_get_response = None + react_step_patch._original_chat_get_response = None + react_step_patch._handler = None + + react_step_patch.apply_react_step_patch(tracer_provider=None) + assert react_step_patch._applied is True + + # Call the wrapped method directly (no instance — staticmethod-style). + coro = _FunctionInvocationLayer.get_response("self-arg", "a", kw="v") + assert asyncio.iscoroutine(coro), ( + "FIL wrapper must return a coroutine so MAF can `await` it" + ) + result = asyncio.get_event_loop().run_until_complete(coro) + assert result[0] == "fil-ok" + + react_step_patch.revert_react_step_patch() + + +def test_chat_wrapper_outside_loop_passes_through(monkeypatch): + """[P0] Outside a react-loop scope, the chat wrapper must return the raw + coroutine produced by the wrapped function (no react_step span). This + preserves the normal ``await layer.get_response(...)`` path used by MAF + when ReAct is not active. + """ + import asyncio + import sys + import types + + async def _chat_get_response(self, *args, **kwargs): + return ("chat-ok", self, args, kwargs) + + class _FunctionInvocationLayer: + get_response = staticmethod(lambda *a, **kw: None) + + class _ChatTelemetryLayer: + get_response = staticmethod(_chat_get_response) + + tools_mod = types.ModuleType("agent_framework._tools") + tools_mod.FunctionInvocationLayer = _FunctionInvocationLayer # type: ignore[attr-defined] + obs_mod = types.ModuleType("agent_framework.observability") + obs_mod.ChatTelemetryLayer = _ChatTelemetryLayer # type: ignore[attr-defined] + af_mod = types.ModuleType("agent_framework") + af_mod._tools = tools_mod # type: ignore[attr-defined] + af_mod.observability = obs_mod # type: ignore[attr-defined] + + monkeypatch.setitem(sys.modules, "agent_framework", af_mod) + monkeypatch.setitem(sys.modules, "agent_framework._tools", tools_mod) + monkeypatch.setitem(sys.modules, "agent_framework.observability", obs_mod) + + react_step_patch.revert_react_step_patch() + react_step_patch._applied = False + react_step_patch._original_fil_get_response = None + react_step_patch._original_chat_get_response = None + react_step_patch._handler = None + + react_step_patch.apply_react_step_patch(tracer_provider=None) + + coro = _ChatTelemetryLayer.get_response("self-arg") + assert asyncio.iscoroutine(coro), ( + "Chat wrapper must pass through the wrapped coroutine unchanged" + ) + result = asyncio.get_event_loop().run_until_complete(coro) + assert result[0] == "chat-ok" + + react_step_patch.revert_react_step_patch() From f5f771ee11553174ac01095f23486bac8866ebab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=81=E5=B1=BF?= Date: Thu, 25 Jun 2026 16:46:23 +0800 Subject: [PATCH 06/21] fix(maf): align telemetry with genai conventions --- instrumentation-genai/README.md | 1 + .../README.rst | 42 ++- .../pyproject.toml | 7 +- .../microsoft_agent_framework/__init__.py | 40 +- .../react_step_patch.py | 85 +++-- .../semantic_conventions.py | 2 +- .../span_processor.py | 346 ++++++++++++------ .../tests/test_processor.py | 120 ++++-- .../tests/test_react_step.py | 200 ++++++++-- pyproject.toml | 3 + scripts/generate_instrumentation_bootstrap.py | 5 + uv.lock | 22 ++ 12 files changed, 640 insertions(+), 233 deletions(-) diff --git a/instrumentation-genai/README.md b/instrumentation-genai/README.md index dbe51b523..faca91aac 100644 --- a/instrumentation-genai/README.md +++ b/instrumentation-genai/README.md @@ -5,6 +5,7 @@ | [opentelemetry-instrumentation-claude-agent-sdk](./opentelemetry-instrumentation-claude-agent-sdk) | claude-agent-sdk >= 0.1.14 | No | development | [opentelemetry-instrumentation-google-genai](./opentelemetry-instrumentation-google-genai) | google-genai >= 1.32.0 | No | development | [opentelemetry-instrumentation-langchain](./opentelemetry-instrumentation-langchain) | langchain >= 0.3.21 | No | development +| [opentelemetry-instrumentation-microsoft-agent-framework](./opentelemetry-instrumentation-microsoft-agent-framework) | agent-framework-core >= 1.0.0 | Yes | development | [opentelemetry-instrumentation-openai-agents-v2](./opentelemetry-instrumentation-openai-agents-v2) | openai-agents >= 0.3.3 | No | development | [opentelemetry-instrumentation-openai-v2](./opentelemetry-instrumentation-openai-v2) | openai >= 1.26.0 | Yes | development | [opentelemetry-instrumentation-vertexai](./opentelemetry-instrumentation-vertexai) | google-cloud-aiplatform >= 1.64 | No | development diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/README.rst b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/README.rst index be54e1c11..9d3337a4b 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/README.rst +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/README.rst @@ -1,6 +1,6 @@ -============================== +===================================================== Microsoft Agent Framework Instrumentation -============================== +===================================================== This package provides OpenTelemetry instrumentation for `Microsoft Agent Framework `_ @@ -13,8 +13,9 @@ described in ``llm-dev/microsoft-agent-framework/investigate/execute.md``: ``ChatTelemetryLayer`` / ``EmbeddingTelemetryLayer`` / ``AgentTelemetryLayer`` / workflow helpers with the ARMS GenAI semantic conventions (``gen_ai.span.kind``, ``gen_ai.operation.name``, normalized - attribute names, ``gen_ai.response.time_to_first_token``, ``StatusCode.OK`` - on success, etc.) and aggregates the 6 ARMS gauges. + attribute names, ``gen_ai.response.time_to_first_token``, etc.) and + aggregates the 6 ARMS gauges. Successful spans keep the OpenTelemetry + default ``UNSET`` status; failed spans keep MAF's ``ERROR`` status. * ``react_step_patch`` (opt-in via ``ARMS_MAF_REACT_STEP_ENABLED=true``) wraps ``FunctionInvocationLayer.get_response`` so that each LLM round-trip inside the ReAct loop emits one ``react step`` span via @@ -24,14 +25,31 @@ Truncation / PII helpers are reused from ``opentelemetry.util.genai.utils`` (``gen_ai_json_dumps``), aligned with ``instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/.../span_processor.py``. +Installation +============ + +Install the instrumentation package together with Microsoft Agent Framework in +the target application environment: + +.. code-block:: console + + pip install opentelemetry-instrumentation-microsoft-agent-framework + pip install agent-framework-core + +The framework dependency is intentionally not included in the package's +``instruments`` extra because current ``agent-framework-core`` releases require +``opentelemetry-api>=1.39`` while this LoongSuite workspace still builds on the +``1.37`` OpenTelemetry API line. + Configuration ============ -============================ ========== ========================================== -Env Default Description -============================ ========== ========================================== -``ARMS_MAF_INSTRUMENTATION_ENABLED`` ``true`` Master switch; ``false`` disables instrumentation. -``ARMS_MAF_SENSITIVE_DATA_ENABLED`` ``false`` Capture inputs/outputs (linked to MAF's ``ENABLE_SENSITIVE_DATA``). -``ARMS_MAF_REACT_STEP_ENABLED`` ``false`` Emit ``react step`` spans (opt-in). -``ARMS_MAF_SLOW_THRESHOLD_MS`` ``1000`` Slow-call threshold in ms. -============================ ========== ========================================== +====================================== ========== ============================================================== +Env Default Description +====================================== ========== ============================================================== +``ARMS_MAF_INSTRUMENTATION_ENABLED`` ``true`` Master switch; ``false`` disables instrumentation. +``ARMS_MAF_SENSITIVE_DATA_ENABLED`` ``false`` Capture inputs/outputs (linked to MAF's sensitive-data option). +``ARMS_MAF_REACT_STEP_ENABLED`` ``false`` Emit ``react step`` spans for non-streaming ReAct tool loops. +``ARMS_MAF_METRICS_ENABLED`` ``true`` Aggregate ARMS GenAI gauges in-process. +``ARMS_MAF_SLOW_THRESHOLD_MS`` ``1000`` Slow-call threshold in ms. +====================================== ========== ============================================================== diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/pyproject.toml b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/pyproject.toml index ffce400f7..525002ba2 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/pyproject.toml +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/pyproject.toml @@ -8,7 +8,7 @@ dynamic = ["version"] description = "OpenTelemetry instrumentation for Microsoft Agent Framework (agent-framework-core)" readme = "README.rst" license = "Apache-2.0" -requires-python = ">=3.9" +requires-python = ">=3.10" authors = [ { name = "ARMS LoongSuite Authors" }, ] @@ -18,7 +18,6 @@ classifiers = [ "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -33,9 +32,7 @@ dependencies = [ ] [project.optional-dependencies] -instruments = [ - "agent-framework-core >= 1.0.0", -] +instruments = [] [project.entry-points.opentelemetry_instrumentor] microsoft_agent_framework = "opentelemetry.instrumentation.microsoft_agent_framework:MicrosoftAgentFrameworkInstrumentor" diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/__init__.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/__init__.py index 2efd3aa0a..1cedd57bc 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/__init__.py +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/__init__.py @@ -49,6 +49,7 @@ class MicrosoftAgentFrameworkInstrumentor(BaseInstrumentor): def __init__(self) -> None: super().__init__() self._processor: Optional[MAFSemanticProcessor] = None + self._tracer_provider: Any = None self._react_applied: bool = False def instrumentation_dependencies(self) -> Collection[str]: @@ -64,7 +65,9 @@ def _instrument(self, **kwargs: Any) -> None: if self._processor is not None: return - tracer_provider = kwargs.get("tracer_provider") or get_tracer_provider() + tracer_provider = ( + kwargs.get("tracer_provider") or get_tracer_provider() + ) meter_provider = kwargs.get("meter_provider") # 1) Enable MAF's built-in OTel instrumentation. ``force=True`` clears @@ -74,7 +77,8 @@ def _instrument(self, **kwargs: Any) -> None: # PII/data redaction by default, per ARMS privacy guardrails). sensitive = bool( kwargs.get( - "enable_sensitive_data", is_sensitive_data_enabled(default=False) + "enable_sensitive_data", + is_sensitive_data_enabled(default=False), ) ) try: @@ -127,7 +131,9 @@ def _instrument(self, **kwargs: Any) -> None: ) # ``span_processors`` may be a tuple (current SDK) or a list. if isinstance(span_processors, tuple): - others = tuple(p for p in span_processors if p is not processor) + others = tuple( + p for p in span_processors if p is not processor + ) new_procs = (processor,) + others if asp is not None: asp._span_processors = new_procs # type: ignore[attr-defined] @@ -141,10 +147,13 @@ def _instrument(self, **kwargs: Any) -> None: logger.debug("prepend processor failed: %s", exc) self._processor = processor + self._tracer_provider = tracer_provider # 3) Optional ReAct step patch (default OFF). react_enabled = bool( - kwargs.get("react_step_enabled", is_react_step_enabled(default=False)) + kwargs.get( + "react_step_enabled", is_react_step_enabled(default=False) + ) ) if react_enabled: try: @@ -159,12 +168,35 @@ def _uninstrument(self, **kwargs: Any) -> None: self._react_applied = False if self._processor is not None: try: + if self._tracer_provider is not None: + _remove_span_processor( + self._tracer_provider, self._processor + ) self._processor.shutdown() except Exception as exc: # pragma: no cover - defensive logger.debug("processor shutdown error: %s", exc) self._processor = None + self._tracer_provider = None # We intentionally do NOT call ``disable_instrumentation()`` — that # would set MAF's sticky ``_user_disabled`` flag and prevent the user # from re-enabling later without ``force=True``. Respects the user's # own MAF observability state. + + +def _remove_span_processor(tracer_provider: Any, processor: Any) -> None: + """Best-effort removal of the processor this instrumentor registered.""" + asp = getattr(tracer_provider, "_active_span_processor", None) + span_processors = ( + getattr(asp, "_span_processors", None) + if asp is not None + else getattr(tracer_provider, "_span_processors", None) + ) + if isinstance(span_processors, tuple): + new_procs = tuple(p for p in span_processors if p is not processor) + if asp is not None: + asp._span_processors = new_procs # type: ignore[attr-defined] + else: + tracer_provider._span_processors = new_procs # type: ignore[attr-defined] + elif isinstance(span_processors, list): + span_processors[:] = [p for p in span_processors if p is not processor] diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/react_step_patch.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/react_step_patch.py index 652d66879..7194ab5af 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/react_step_patch.py +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/react_step_patch.py @@ -25,6 +25,7 @@ from __future__ import annotations import contextvars +import inspect import logging from typing import Any @@ -54,7 +55,9 @@ def _get_extended_handler(tracer_provider: Any = None) -> Any: get_extended_telemetry_handler, ) - _handler = get_extended_telemetry_handler(tracer_provider=tracer_provider) + _handler = get_extended_telemetry_handler( + tracer_provider=tracer_provider + ) return _handler @@ -66,8 +69,12 @@ def apply_react_step_patch(tracer_provider: Any = None) -> None: try: import wrapt # type: ignore - from agent_framework._tools import FunctionInvocationLayer # type: ignore - from agent_framework.observability import ChatTelemetryLayer # type: ignore + from agent_framework._tools import ( + FunctionInvocationLayer, # type: ignore + ) + from agent_framework.observability import ( + ChatTelemetryLayer, # type: ignore + ) except (ImportError, AttributeError) as exc: logger.warning( "ReAct step patch skipped: MAF internals not found (%s). " @@ -91,30 +98,20 @@ def apply_react_step_patch(tracer_provider: Any = None) -> None: handler = _get_extended_handler(tracer_provider) - # The wrappers are *synchronous* functions that return coroutines. We - # deliberately avoid ``@wrapt.decorator`` on ``async def`` here: - # ``wrapt.wrap_function_wrapper`` installs a ``FunctionWrapper`` whose - # ``__call__`` invokes our wrapper and returns whatever it returns. If our - # wrapper were ``async def``, the FunctionWrapper would still return a - # coroutine — *but* MAF 1.0.0's ``_call_chat_client`` is itself ``async def`` - # and is called as ``await layer.get_response(...)`` from ``_agents.py``. - # Empirically (validation report P0) the async-wrapped variant does NOT - # produce a coroutine on ``await`` — the call site raises ``TypeError``. - # Returning a coroutine from a sync wrapper is the simplest, robust fix: - # the caller's ``await`` resolves the coroutine normally. - # - # ContextVar tokens are set *inside* the coroutine body (not at wrapper - # entry) so set/reset share the same asyncio Task context. asyncio copies - # the context when a Task is created; tokens created outside the task - # cannot be reset inside it (``ValueError: created in a different - # Context``). Doing the ``set`` inside the coroutine guarantees the token - # belongs to whichever context ends up running the coroutine. + # These MAF methods are synchronous dispatchers: stream=False returns an + # awaitable while stream=True returns a ResponseStream. The wrapper must + # preserve that public contract, so it only wraps awaitables and passes + # streaming ResponseStream values through unchanged. def _fil_wrapper(wrapped, instance, args, kwargs): # type: ignore[no-untyped-def] + result = wrapped(*args, **kwargs) + if _is_response_stream(result) or not inspect.isawaitable(result): + return result + async def _scoped(): # type: ignore[no-untyped-def] token_active = _maf_react_loop_active.set(True) token_counter = _maf_react_step_counter.set(0) try: - return await wrapped(*args, **kwargs) + return await result finally: _maf_react_loop_active.reset(token_active) _maf_react_step_counter.reset(token_counter) @@ -122,12 +119,15 @@ async def _scoped(): # type: ignore[no-untyped-def] return _scoped() def _chat_wrapper(wrapped, instance, args, kwargs): # type: ignore[no-untyped-def] - # Read the loop-active flag synchronously (cheap; copied into the - # coroutine below). When False we pass the wrapped coroutine through - # unchanged — ``wrapped`` is a coroutine function so calling it returns - # a coroutine and the caller awaits it directly. + result = wrapped(*args, **kwargs) + + # Read the loop-active flag synchronously. When False we pass the + # wrapped result through unchanged. When True, only awaitables are + # wrapped; streaming ResponseStream values keep their original type. if not _maf_react_loop_active.get(): - return wrapped(*args, **kwargs) + return result + if _is_response_stream(result) or not inspect.isawaitable(result): + return result # Pre-compute the round number for this call (read before the # coroutine body so subsequent chat calls in the same loop see the @@ -146,12 +146,12 @@ async def _step_scoped(): # type: ignore[no-untyped-def] try: with local_handler.react_step(step_inv) as step: try: - result = await wrapped(*args, **kwargs) + response = await result # Best-effort finish_reason extraction from the response. - finish = _extract_finish_reason(result) + finish = _extract_finish_reason(response) if finish is not None: step.finish_reason = finish - return result + return response except Exception: step.finish_reason = "error" raise @@ -198,6 +198,19 @@ def _unwrap_to_function(func: Any) -> Any: return cur +def _is_response_stream(result: Any) -> bool: + """Return True for MAF ResponseStream-like values. + + ``ResponseStream`` is awaitable as a convenience, so ``inspect.isawaitable`` + alone cannot distinguish it from the non-streaming coroutine path. + """ + return ( + hasattr(result, "map") + and hasattr(result, "__aiter__") + and hasattr(result, "get_final_response") + ) + + def _extract_finish_reason(result: Any) -> Any: """Best-effort extraction of a finish_reason string from a ChatResponse.""" try: @@ -224,12 +237,18 @@ def revert_react_step_patch() -> None: if not _applied: return try: - from agent_framework._tools import FunctionInvocationLayer # type: ignore - from agent_framework.observability import ChatTelemetryLayer # type: ignore + from agent_framework._tools import ( + FunctionInvocationLayer, # type: ignore + ) + from agent_framework.observability import ( + ChatTelemetryLayer, # type: ignore + ) if _original_fil_get_response is not None: try: - FunctionInvocationLayer.get_response = _original_fil_get_response # type: ignore[assignment] + FunctionInvocationLayer.get_response = ( + _original_fil_get_response # type: ignore[assignment] + ) except Exception: pass if _original_chat_get_response is not None: diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py index cba22abfb..c1b8d20d6 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py @@ -111,8 +111,8 @@ class GenAIOperation: GEN_AI_PROVIDER_NAME = "gen_ai.provider.name" GEN_AI_REQUEST_MODEL = "gen_ai.request.model" GEN_AI_RESPONSE_MODEL = "gen_ai.response.model" +GEN_AI_RESPONSE_FINISH_REASONS = "gen_ai.response.finish_reasons" GEN_AI_RESPONSE_TTFT = "gen_ai.response.time_to_first_token" -GEN_AI_USER_TTFT = "gen_ai.user.time_to_first_token" GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens" GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens" GEN_AI_REACT_ROUND = "gen_ai.react.round" diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py index 924fad16b..2046fb5e4 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py @@ -13,8 +13,7 @@ 3. Backfills ``gen_ai.response.time_to_first_token`` from the first streaming chunk event timestamp. 4. Normalizes ``gen_ai.provider.name`` (``azure_openai`` → ``openai``). -5. Sets ``StatusCode.OK`` on successful spans (MAF only sets ``ERROR``). -6. Aggregates 6 ARMS gauges (``genai_calls_count`` / ``genai_calls_duration_seconds`` +5. Aggregates 6 ARMS gauges (``genai_calls_count`` / ``genai_calls_duration_seconds`` / ``genai_calls_error_count`` / ``genai_calls_slow_count`` / ``genai_llm_first_token_seconds`` / ``genai_llm_usage_tokens``) in-process, exposed via observable gauges. @@ -27,38 +26,39 @@ from __future__ import annotations +import json import logging +import threading from collections import defaultdict from typing import Any, Dict, Optional, Tuple from opentelemetry.context import Context from opentelemetry.metrics import ObservableGauge, get_meter -from opentelemetry.sdk.trace import SpanProcessor -from opentelemetry.sdk.trace import TracerProvider # noqa: F401 (typing hint) -from opentelemetry.trace import Span as OtelSpan, Status, StatusCode +from opentelemetry.sdk.trace import ( + SpanProcessor, + TracerProvider, # noqa: F401 (typing hint) +) +from opentelemetry.trace import Span as OtelSpan +from opentelemetry.trace import SpanKind, StatusCode from opentelemetry.trace.span import TraceState # noqa: F401 from opentelemetry.util.genai.utils import gen_ai_json_dumps -from opentelemetry.util.types import AttributeValue from .semantic_conventions import ( ERROR_TYPE, - FRAMEWORK_NAME, - GEN_AI_FRAMEWORK, GEN_AI_OPERATION_NAME, GEN_AI_PROVIDER_NAME, - GEN_AI_REACT_ROUND, GEN_AI_REQUEST_MODEL, + GEN_AI_RESPONSE_FINISH_REASONS, GEN_AI_RESPONSE_MODEL, GEN_AI_RESPONSE_TTFT, GEN_AI_SPAN_KIND, GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, - GEN_AI_USER_TTFT, - GenAIOperation, - GenAISpanKind, MAF_ATTR_RENAME_MAP, MAF_SPAN_NAME_PREFIXES, PROVIDER_NAME_NORMALIZE, + GenAIOperation, + GenAISpanKind, ) logger = logging.getLogger(__name__) @@ -244,6 +244,30 @@ def _normalize_provider(value: Any) -> Optional[str]: return value +def _normalize_finish_reasons(live_span: OtelSpan, readable: Any) -> None: + """Normalize JSON-encoded finish reasons to an OTel string array.""" + value = _attr_value(readable, GEN_AI_RESPONSE_FINISH_REASONS) + if not isinstance(value, str): + return + try: + parsed = json.loads(value) + except (TypeError, ValueError): + return + if isinstance(parsed, list) and all( + isinstance(item, str) for item in parsed + ): + _set_attr(live_span, GEN_AI_RESPONSE_FINISH_REASONS, parsed) + + +def _set_span_kind(live_span: OtelSpan, readable: Any, kind: SpanKind) -> None: + """Mutate the SDK span kind before downstream exporters receive the span.""" + for target in (readable, live_span): + try: + target._kind = kind # type: ignore[attr-defined] + except Exception: + pass + + _MCP_METHOD_NAME_ATTR = "mcp.method.name" @@ -269,6 +293,20 @@ def _is_mcp_span(readable: Any) -> bool: return False +def _is_maf_span(name: str, operation: Optional[str], readable: Any) -> bool: + """Return True when the span carries a Microsoft Agent Framework signal.""" + if operation: + return True + if _is_mcp_span(readable): + return True + if name == _REACT_STEP_NAME: + return True + if any(name.startswith(prefix) for prefix in MAF_SPAN_NAME_PREFIXES): + return True + attrs = getattr(readable, "attributes", None) or {} + return any(key in attrs for key in MAF_ATTR_RENAME_MAP) + + def _classify_span( name: str, operation: Optional[str], readable: Any ) -> Tuple[str, str]: @@ -305,9 +343,15 @@ def _classify_span( if name == _REACT_STEP_NAME: op = GenAIOperation.REACT else: - op = GenAIOperation.WORKFLOW # safe default for MAF internal spans - - if op == GenAIOperation.CHAT or op == GenAIOperation.TEXT_COMPLETION or op == GenAIOperation.GENERATE_CONTENT: + op = ( + GenAIOperation.WORKFLOW + ) # safe default for MAF internal spans + + if ( + op == GenAIOperation.CHAT + or op == GenAIOperation.TEXT_COMPLETION + or op == GenAIOperation.GENERATE_CONTENT + ): return GenAISpanKind.LLM, op if op == GenAIOperation.EMBEDDINGS: return GenAISpanKind.EMBEDDING, op @@ -391,13 +435,23 @@ class _Counters: def __init__(self) -> None: self.calls_count: Dict[Tuple[str, str], int] = defaultdict(int) - self.calls_duration_ns_sum: Dict[Tuple[str, str], int] = defaultdict(int) + self.calls_duration_ns_sum: Dict[Tuple[str, str], int] = defaultdict( + int + ) self.calls_error_count: Dict[Tuple[str, str], int] = defaultdict(int) self.calls_slow_count: Dict[Tuple[str, str], int] = defaultdict(int) - self.llm_first_token_ns_sum: Dict[Tuple[str, str], int] = defaultdict(int) - self.llm_first_token_count: Dict[Tuple[str, str], int] = defaultdict(int) - self.llm_usage_input_tokens: Dict[Tuple[str, str], int] = defaultdict(int) - self.llm_usage_output_tokens: Dict[Tuple[str, str], int] = defaultdict(int) + self.llm_first_token_ns_sum: Dict[Tuple[str, str], int] = defaultdict( + int + ) + self.llm_first_token_count: Dict[Tuple[str, str], int] = defaultdict( + int + ) + self.llm_usage_input_tokens: Dict[Tuple[str, str], int] = defaultdict( + int + ) + self.llm_usage_output_tokens: Dict[Tuple[str, str], int] = defaultdict( + int + ) class MAFSemanticProcessor(SpanProcessor): @@ -415,6 +469,7 @@ def __init__( self._slow_threshold_ns = int(slow_threshold_ms) * 1_000_000 self._capture_sensitive = capture_sensitive_data self._counters = _Counters() + self._counter_lock = threading.Lock() self._meter = None self._gauges: list[ObservableGauge] = [] self._metrics_enabled = metrics_enabled @@ -434,63 +489,115 @@ def _init_metrics(self, meter_provider: Any) -> None: c = self._counters def _calls_cb(options): - for (model, kind), count in c.calls_count.items(): - yield _obs( - count, - {"modelName": model or "unknown", "spanKind": kind}, - ) + observations = [] + with self._counter_lock: + for (model, kind), count in c.calls_count.items(): + observations.append( + _obs( + count, + { + "modelName": model or "unknown", + "spanKind": kind, + }, + ) + ) + yield from observations def _duration_cb(options): - for (model, kind), total in c.calls_duration_ns_sum.items(): - count = max(c.calls_count.get((model, kind), 0), 1) - yield _obs( - total / count / 1e9, - {"modelName": model or "unknown", "spanKind": kind}, - ) + observations = [] + with self._counter_lock: + for (model, kind), total in c.calls_duration_ns_sum.items(): + count = max(c.calls_count.get((model, kind), 0), 1) + observations.append( + _obs( + total / count / 1e9, + { + "modelName": model or "unknown", + "spanKind": kind, + }, + ) + ) + yield from observations def _error_cb(options): - for (model, kind), count in c.calls_error_count.items(): - yield _obs( - count, - {"modelName": model or "unknown", "spanKind": kind}, - ) + observations = [] + with self._counter_lock: + for (model, kind), count in c.calls_error_count.items(): + observations.append( + _obs( + count, + { + "modelName": model or "unknown", + "spanKind": kind, + }, + ) + ) + yield from observations def _slow_cb(options): - for (model, kind), count in c.calls_slow_count.items(): - yield _obs( - count, - {"modelName": model or "unknown", "spanKind": kind}, - ) + observations = [] + with self._counter_lock: + for (model, kind), count in c.calls_slow_count.items(): + observations.append( + _obs( + count, + { + "modelName": model or "unknown", + "spanKind": kind, + }, + ) + ) + yield from observations def _ttft_cb(options): - for (model, kind), total in c.llm_first_token_ns_sum.items(): - count = max(c.llm_first_token_count.get((model, kind), 0), 1) - yield _obs( - total / count / 1e9, - {"modelName": model or "unknown", "spanKind": kind}, - ) + observations = [] + with self._counter_lock: + for (model, kind), total in c.llm_first_token_ns_sum.items(): + count = max( + c.llm_first_token_count.get((model, kind), 0), 1 + ) + observations.append( + _obs( + total / count / 1e9, + { + "modelName": model or "unknown", + "spanKind": kind, + }, + ) + ) + yield from observations def _tokens_input_cb(options): - for (model, kind), total in c.llm_usage_input_tokens.items(): - yield _obs( - total, - { - "modelName": model or "unknown", - "spanKind": kind, - "usageType": "input", - }, - ) + observations = [] + with self._counter_lock: + for (model, kind), total in c.llm_usage_input_tokens.items(): + observations.append( + _obs( + total, + { + "modelName": model or "unknown", + "spanKind": kind, + "usageType": "input", + }, + ) + ) + yield from observations def _tokens_output_cb(options): - for (model, kind), total in c.llm_usage_output_tokens.items(): - yield _obs( - total, - { - "modelName": model or "unknown", - "spanKind": kind, - "usageType": "output", - }, - ) + observations = [] + with self._counter_lock: + for (model, kind), total in c.llm_usage_output_tokens.items(): + observations.append( + _obs( + total, + { + "modelName": model or "unknown", + "spanKind": kind, + "usageType": "output", + }, + ) + ) + yield from observations self._gauges.append( self._meter.create_observable_gauge( @@ -579,6 +686,8 @@ def on_end(self, span: Any) -> None: name = span.name or "" existing_op = _attr_value(span, GEN_AI_OPERATION_NAME) existing_op = existing_op if isinstance(existing_op, str) else None + if not _is_maf_span(name, existing_op, span): + return span_kind, op_name = _classify_span(name, existing_op, span) # 1) gen_ai.span.kind (only set if not already present) @@ -603,27 +712,26 @@ def on_end(self, span: Any) -> None: }: _set_attr(live, GEN_AI_OPERATION_NAME, op_name) - # 3) gen_ai.framework (always — ARMS extension) - if not _attr_value(span, GEN_AI_FRAMEWORK): - _set_attr(live, GEN_AI_FRAMEWORK, FRAMEWORK_NAME) - - # 4) Rename MAF private-prefix attributes + # 3) Rename MAF private-prefix attributes _rename_maf_attrs(live, span) - # 5) Normalize provider.name + # 4) Normalize provider.name provider = _attr_value(span, GEN_AI_PROVIDER_NAME) normalized = _normalize_provider(provider) if normalized is not None and normalized != provider: _set_attr(live, GEN_AI_PROVIDER_NAME, normalized) + # 5) Normalize finish reasons written by MAF as a JSON string. + _normalize_finish_reasons(live, span) + # 6) TTFT backfill for LLM spans with streaming events if span_kind == GenAISpanKind.LLM: + _set_span_kind(live, span, SpanKind.CLIENT) ttft = _ttft_from_events(span) if ttft is not None and not _attr_value( span, GEN_AI_RESPONSE_TTFT ): _set_attr(live, GEN_AI_RESPONSE_TTFT, ttft) - _set_attr(live, GEN_AI_USER_TTFT, ttft) # 7) ENTRY detection: a root invoke_agent span with no parent becomes # the trace entry point. @@ -640,24 +748,9 @@ def on_end(self, span: Any) -> None: # unless an application-level ENTRY span exists. pass - # 8) Status: set OK on success (MAF only sets ERROR). The SDK - # passes a ``ReadableSpan`` snapshot to ``on_end``; its - # ``_status`` is a separate field from the live Span's, so we - # mutate the ReadableSpan's ``_status`` directly (and the live - # Span's too, for symmetry). The shared ``_attributes`` dict is - # mutated via ``_set_attr`` above and is visible to exporters. - current_status = getattr(span, "status", None) - status_code = getattr(current_status, "status_code", None) - if status_code != StatusCode.ERROR: - ok_status = Status(StatusCode.OK) - try: - span._status = ok_status # type: ignore[attr-defined] - except Exception as exc: # pragma: no cover - defensive - logger.debug("set_status OK on readable failed: %s", exc) - try: - live._status = ok_status # type: ignore[attr-defined] - except Exception: - pass + # 8) Status: MAF already sets ERROR on failed spans. Successful + # spans are left UNSET, matching the OTel SDK default and Weaver's + # validation model. # 9) error.type already set by MAF via capture_exception; nothing to do. @@ -680,37 +773,48 @@ def _aggregate_metrics( model = _attr_value(readable, GEN_AI_RESPONSE_MODEL) model = model if isinstance(model, str) else "unknown" key = (model, span_kind) - self._counters.calls_count[key] += 1 - - start = getattr(readable, "start_time", None) - end = getattr(readable, "end_time", None) - if start is not None and end is not None: - try: - duration_ns = int(end - start) - self._counters.calls_duration_ns_sum[key] += duration_ns - if duration_ns >= self._slow_threshold_ns: - self._counters.calls_slow_count[key] += 1 - except (TypeError, ValueError): - pass - - current_status = getattr(readable, "status", None) - status_code = getattr(current_status, "status_code", None) - if status_code == StatusCode.ERROR: - self._counters.calls_error_count[key] += 1 - elif _attr_value(readable, ERROR_TYPE): - self._counters.calls_error_count[key] += 1 - - if span_kind == GenAISpanKind.LLM: - ttft = _attr_value(readable, GEN_AI_RESPONSE_TTFT) - if isinstance(ttft, (int, float)) and ttft > 0: - self._counters.llm_first_token_ns_sum[key] += int(ttft) - self._counters.llm_first_token_count[key] += 1 - input_tokens = _attr_value(readable, GEN_AI_USAGE_INPUT_TOKENS) - if isinstance(input_tokens, (int, float)): - self._counters.llm_usage_input_tokens[key] += int(input_tokens) - output_tokens = _attr_value(readable, GEN_AI_USAGE_OUTPUT_TOKENS) - if isinstance(output_tokens, (int, float)): - self._counters.llm_usage_output_tokens[key] += int(output_tokens) + with self._counter_lock: + self._counters.calls_count[key] += 1 + + start = getattr(readable, "start_time", None) + end = getattr(readable, "end_time", None) + if start is not None and end is not None: + try: + duration_ns = int(end - start) + self._counters.calls_duration_ns_sum[key] += ( + duration_ns + ) + if duration_ns >= self._slow_threshold_ns: + self._counters.calls_slow_count[key] += 1 + except (TypeError, ValueError): + pass + + current_status = getattr(readable, "status", None) + status_code = getattr(current_status, "status_code", None) + if status_code == StatusCode.ERROR: + self._counters.calls_error_count[key] += 1 + elif _attr_value(readable, ERROR_TYPE): + self._counters.calls_error_count[key] += 1 + + if span_kind == GenAISpanKind.LLM: + ttft = _attr_value(readable, GEN_AI_RESPONSE_TTFT) + if isinstance(ttft, (int, float)) and ttft > 0: + self._counters.llm_first_token_ns_sum[key] += int(ttft) + self._counters.llm_first_token_count[key] += 1 + input_tokens = _attr_value( + readable, GEN_AI_USAGE_INPUT_TOKENS + ) + if isinstance(input_tokens, (int, float)): + self._counters.llm_usage_input_tokens[key] += int( + input_tokens + ) + output_tokens = _attr_value( + readable, GEN_AI_USAGE_OUTPUT_TOKENS + ) + if isinstance(output_tokens, (int, float)): + self._counters.llm_usage_output_tokens[key] += int( + output_tokens + ) except Exception as exc: # pragma: no cover - defensive logger.debug("MAF metrics aggregation failed: %s", exc) diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_processor.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_processor.py index 039c209c5..fdb4c06dd 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_processor.py +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_processor.py @@ -6,7 +6,7 @@ - Reclassifies ``executor.process`` spans by ``executor.type``. - Normalizes ``gen_ai.provider.name``. - Backfills ``gen_ai.response.time_to_first_token`` from streaming events. -- Sets ``StatusCode.OK`` on success, preserves ``ERROR`` on failure. +- Leaves successful spans with the SDK's default status, preserves ``ERROR`` on failure. - Aggregates metrics counters. """ @@ -14,28 +14,26 @@ import time -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import SimpleSpanProcessor -from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( - InMemorySpanExporter, -) -from opentelemetry.trace import Status, StatusCode - -from opentelemetry.instrumentation.microsoft_agent_framework.span_processor import ( - MAFSemanticProcessor, -) from opentelemetry.instrumentation.microsoft_agent_framework.semantic_conventions import ( - GEN_AI_FRAMEWORK, GEN_AI_OPERATION_NAME, GEN_AI_PROVIDER_NAME, + GEN_AI_RESPONSE_FINISH_REASONS, GEN_AI_RESPONSE_TTFT, GEN_AI_SPAN_KIND, GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, - GEN_AI_USER_TTFT, GenAIOperation, GenAISpanKind, ) +from opentelemetry.instrumentation.microsoft_agent_framework.span_processor import ( + MAFSemanticProcessor, +) +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) +from opentelemetry.trace import SpanKind, Status, StatusCode def _setup(): @@ -43,7 +41,9 @@ def _setup(): tp = TracerProvider() exporter = InMemorySpanExporter() processor = MAFSemanticProcessor( - meter_provider=None, metrics_enabled=False, capture_sensitive_data=False + meter_provider=None, + metrics_enabled=False, + capture_sensitive_data=False, ) tp.add_span_processor(processor) tp.add_span_processor(SimpleSpanProcessor(exporter)) @@ -69,8 +69,8 @@ def test_llm_span_gets_llm_kind_and_chat_operation(): s = spans[0] assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.LLM assert s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.CHAT - assert s.attributes.get(GEN_AI_FRAMEWORK) == "microsoft-agent-framework" - assert s.status.status_code == StatusCode.OK + assert s.kind == SpanKind.CLIENT + assert s.status.status_code == StatusCode.UNSET def test_tool_span_gets_tool_kind(): @@ -84,7 +84,9 @@ def test_tool_span_gets_tool_kind(): def test_embedding_span(): tp, tracer, exporter, _ = _setup() - with tracer.start_as_current_span("embeddings text-embedding-3-small") as span: + with tracer.start_as_current_span( + "embeddings text-embedding-3-small" + ) as span: span.set_attribute(GEN_AI_OPERATION_NAME, GenAIOperation.EMBEDDINGS) span.set_attribute("gen_ai.request.model", "text-embedding-3-small") spans = _flush(exporter) @@ -139,7 +141,9 @@ def test_executor_process_agent_executor_becomes_agent(): spans = _flush(exporter) s = spans[0] assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.AGENT - assert s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.INVOKE_AGENT + assert ( + s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.INVOKE_AGENT + ) def test_executor_process_unknown_executor_stays_chain(): @@ -188,9 +192,16 @@ def test_ttft_backfill_from_first_event(): spans = _flush(exporter) s = spans[0] ttft = s.attributes.get(GEN_AI_RESPONSE_TTFT) - user_ttft = s.attributes.get(GEN_AI_USER_TTFT) assert ttft is not None and ttft > 0 - assert user_ttft == ttft + + +def test_finish_reasons_json_string_normalized_to_array(): + tp, tracer, exporter, _ = _setup() + with tracer.start_as_current_span("chat gpt-4o") as span: + span.set_attribute(GEN_AI_OPERATION_NAME, GenAIOperation.CHAT) + span.set_attribute(GEN_AI_RESPONSE_FINISH_REASONS, '["stop"]') + spans = _flush(exporter) + assert spans[0].attributes.get(GEN_AI_RESPONSE_FINISH_REASONS) == ("stop",) def _setup_with_metrics(): @@ -237,13 +248,23 @@ def test_metrics_counters_incremented_on_llm_span(): span.set_attribute(GEN_AI_USAGE_OUTPUT_TOKENS, 7) _ = _flush(exporter) assert processor._counters.calls_count[("gpt-4o", GenAISpanKind.LLM)] == 1 - assert processor._counters.llm_usage_input_tokens[("gpt-4o", GenAISpanKind.LLM)] == 5 - assert processor._counters.llm_usage_output_tokens[("gpt-4o", GenAISpanKind.LLM)] == 7 + assert ( + processor._counters.llm_usage_input_tokens[ + ("gpt-4o", GenAISpanKind.LLM) + ] + == 5 + ) + assert ( + processor._counters.llm_usage_output_tokens[ + ("gpt-4o", GenAISpanKind.LLM) + ] + == 7 + ) def test_react_step_span_classification(): tp, tracer, exporter, _ = _setup() - with tracer.start_as_current_span("react step") as span: + with tracer.start_as_current_span("react step"): # When emitted by our react_step_patch, the handler sets # gen_ai.operation.name=react and gen_ai.span.kind=STEP itself. But # if the processor sees a "react step" name without op set, it should @@ -260,15 +281,43 @@ def test_uninstrument_releases_processor(): MicrosoftAgentFrameworkInstrumentor, ) - # Without MAF installed we just exercise that _uninstrument does not raise - # and clears state. We construct the instrumentor and call _uninstrument - # without _instrument to ensure idempotent teardown. inst = MicrosoftAgentFrameworkInstrumentor() inst._uninstrument() assert inst._processor is None assert inst._react_applied is False +def test_uninstrument_removes_registered_processor_from_provider(): + from opentelemetry.instrumentation.microsoft_agent_framework import ( + MicrosoftAgentFrameworkInstrumentor, + ) + + tp = TracerProvider() + inst = MicrosoftAgentFrameworkInstrumentor() + inst._instrument(tracer_provider=tp, react_step_enabled=False) + processor = inst._processor + assert processor is not None + inst._uninstrument() + asp = getattr(tp, "_active_span_processor", None) + procs = ( + getattr(asp, "_span_processors", None) + if asp is not None + else getattr(tp, "_span_processors", None) + ) + assert procs is not None and processor not in procs + + +def test_non_maf_span_is_left_untouched(): + tp, tracer, exporter, processor = _setup_with_metrics() + with tracer.start_as_current_span("http request") as span: + span.set_attribute("http.method", "GET") + spans = _flush(exporter) + s = spans[0] + assert GEN_AI_SPAN_KIND not in s.attributes + assert GEN_AI_OPERATION_NAME not in s.attributes + assert not processor._counters.calls_count + + def test_maf_dict_attribute_is_serialized_via_gen_ai_json_dumps(): """Dict/list attribute values written into ``_attributes`` by MAF under private prefixes must be JSON-serialized via @@ -350,7 +399,9 @@ def test_mcp_span_via_client_kind_and_mcp_attr_fallback(): from opentelemetry.trace import SpanKind tp, tracer, exporter, _ = _setup() - with tracer.start_as_current_span("initialize", kind=SpanKind.CLIENT) as span: + with tracer.start_as_current_span( + "initialize", kind=SpanKind.CLIENT + ) as span: span.set_attribute("mcp.protocol.version", "2024-11-05") spans = _flush(exporter) s = spans[0] @@ -364,11 +415,14 @@ def test_non_mcp_client_span_is_not_misclassified_as_mcp(): from opentelemetry.trace import SpanKind tp, tracer, exporter, _ = _setup() - with tracer.start_as_current_span("http request", kind=SpanKind.CLIENT) as span: + with tracer.start_as_current_span( + "http request", kind=SpanKind.CLIENT + ) as span: span.set_attribute("http.method", "GET") spans = _flush(exporter) s = spans[0] - assert s.attributes.get(GEN_AI_SPAN_KIND) != GenAISpanKind.CLIENT + assert GEN_AI_SPAN_KIND not in s.attributes + assert GEN_AI_OPERATION_NAME not in s.attributes def test_mcp_span_op_name_overridden_to_mcp_when_maf_writes_execute_tool(): @@ -410,9 +464,9 @@ def test_provider_normalization_microsoft_agent_framework_to_openai(): span.set_attribute(GEN_AI_OPERATION_NAME, GenAIOperation.INVOKE_AGENT) span.set_attribute(GEN_AI_PROVIDER_NAME, "microsoft.agent_framework") spans = _flush(exporter) - assert ( - spans[0].attributes.get(GEN_AI_PROVIDER_NAME) == "openai" - ), "AGENT provider.name should normalize from microsoft.agent_framework to openai" + assert spans[0].attributes.get(GEN_AI_PROVIDER_NAME) == "openai", ( + "AGENT provider.name should normalize from microsoft.agent_framework to openai" + ) def test_provider_normalization_case_insensitive_variant(): @@ -444,7 +498,7 @@ def test_instrument_prepends_processor_before_existing_exporters(): """[P5] When exporters were registered before ``instrument()`` (the common bootstrap order: provider → exporter processor → instrument()), the MAF semantic processor must run FIRST in the pipeline so its ``on_end`` - enrichments (gen_ai.span.kind, operation.name, framework, rename map, + enrichments (gen_ai.span.kind, operation.name, rename map, provider normalization) are visible to those exporters. Without the prepend, an exporter that captured the span before our ``on_end`` would ship an un-enriched span. diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_react_step.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_react_step.py index e5e3f5141..bb1b7804e 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_react_step.py +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_react_step.py @@ -15,16 +15,19 @@ import logging +from opentelemetry.instrumentation.microsoft_agent_framework import ( + react_step_patch, +) from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) -from opentelemetry.util.genai.extended_handler import get_extended_telemetry_handler +from opentelemetry.util.genai.extended_handler import ( + get_extended_telemetry_handler, +) from opentelemetry.util.genai.extended_types import ReactStepInvocation -from opentelemetry.instrumentation.microsoft_agent_framework import react_step_patch - def test_module_imports_without_maf(): # Importing the module should not raise even though MAF is absent. @@ -32,9 +35,31 @@ def test_module_imports_without_maf(): assert hasattr(react_step_patch, "revert_react_step_patch") -def test_apply_is_noop_when_maf_missing(caplog): +def _reset_extended_handler_singletons(): + react_step_patch._handler = None + if hasattr(get_extended_telemetry_handler, "_default_handler"): + delattr(get_extended_telemetry_handler, "_default_handler") + + +def test_apply_is_noop_when_maf_missing(caplog, monkeypatch): # MAF is not installed in this test env, so apply should warn and return. + import builtins + + original_import = builtins.__import__ + + def _blocked_import( + name, globals_=None, locals_=None, fromlist=(), level=0 + ): + if name.startswith("agent_framework"): + raise ImportError("blocked agent_framework") + return original_import(name, globals_, locals_, fromlist, level) + + monkeypatch.setattr(builtins, "__import__", _blocked_import) react_step_patch.revert_react_step_patch() # ensure clean state + react_step_patch._applied = False + react_step_patch._original_fil_get_response = None + react_step_patch._original_chat_get_response = None + _reset_extended_handler_singletons() with caplog.at_level(logging.WARNING): react_step_patch.apply_react_step_patch(tracer_provider=None) assert react_step_patch._applied is False @@ -54,6 +79,7 @@ def test_handler_react_step_emits_step_span(): tp = TracerProvider() exporter = InMemorySpanExporter() tp.add_span_processor(SimpleSpanProcessor(exporter)) + _reset_extended_handler_singletons() handler = get_extended_telemetry_handler(tracer_provider=tp) step_inv = ReactStepInvocation(round=3) @@ -78,7 +104,6 @@ def _install_fake_maf_modules(monkeypatch): Returns ``(fil_cls, chat_cls)`` — the two fake classes with their original ``get_response`` callables recorded. """ - import asyncio import sys import types @@ -104,9 +129,7 @@ class _ChatTelemetryLayer: monkeypatch.setitem(sys.modules, "agent_framework", af_mod) monkeypatch.setitem(sys.modules, "agent_framework._tools", tools_mod) - monkeypatch.setitem( - sys.modules, "agent_framework.observability", obs_mod - ) + monkeypatch.setitem(sys.modules, "agent_framework.observability", obs_mod) return _FunctionInvocationLayer, _ChatTelemetryLayer @@ -127,7 +150,7 @@ def test_react_patch_apply_revert_apply_no_multi_wrap(monkeypatch): react_step_patch._applied = False react_step_patch._original_fil_get_response = None react_step_patch._original_chat_get_response = None - react_step_patch._handler = None + _reset_extended_handler_singletons() fil_before = fil_cls.get_response chat_before = chat_cls.get_response @@ -137,16 +160,24 @@ def test_react_patch_apply_revert_apply_no_multi_wrap(monkeypatch): assert react_step_patch._applied is True fil_after_1 = fil_cls.get_response chat_after_1 = chat_cls.get_response - assert fil_after_1 is not fil_before, "wrapt did not replace FIL.get_response" - assert chat_after_1 is not chat_before, "wrapt did not replace Chat.get_response" + assert fil_after_1 is not fil_before, ( + "wrapt did not replace FIL.get_response" + ) + assert chat_after_1 is not chat_before, ( + "wrapt did not replace Chat.get_response" + ) # 2) revert react_step_patch.revert_react_step_patch() assert react_step_patch._applied is False # After revert the attribute must point to the *original* (unwrapped) # function — not to the wrapper. - assert fil_cls.get_response is fil_before, "revert did not restore FIL.get_response" - assert chat_cls.get_response is chat_before, "revert did not restore Chat.get_response" + assert fil_cls.get_response is fil_before, ( + "revert did not restore FIL.get_response" + ) + assert chat_cls.get_response is chat_before, ( + "revert did not restore Chat.get_response" + ) # 3) apply again react_step_patch.apply_react_step_patch(tracer_provider=None) @@ -162,7 +193,10 @@ def test_react_patch_apply_revert_apply_no_multi_wrap(monkeypatch): # original in a bounded number of steps (== 1, since revert restored it). depth = 0 cur = fil_after_2 - while getattr(cur, "__wrapped__", None) is not None and cur.__wrapped__ is not cur: + while ( + getattr(cur, "__wrapped__", None) is not None + and cur.__wrapped__ is not cur + ): cur = cur.__wrapped__ depth += 1 assert depth < 8, "wrapper chain too deep — multi-wrap detected" @@ -227,7 +261,7 @@ class _ChatTelemetryLayer: react_step_patch._applied = False react_step_patch._original_fil_get_response = None react_step_patch._original_chat_get_response = None - react_step_patch._handler = None + _reset_extended_handler_singletons() react_step_patch.apply_react_step_patch(tracer_provider=None) assert react_step_patch._applied is True @@ -243,13 +277,78 @@ class _ChatTelemetryLayer: react_step_patch.revert_react_step_patch() +def test_fil_wrapper_preserves_streaming_response_type(monkeypatch): + """When MAF calls ``get_response(stream=True)``, the wrapped method returns + a ResponseStream-like object, not an awaitable. The patch must preserve + that contract so ``agent.run(..., stream=True)`` can keep using ``.map`` + and async iteration. + """ + import asyncio + import sys + import types + + class _Stream: + def __await__(self): + async def _done(): + return self + + return _done().__await__() + + def __aiter__(self): + return self + + async def __anext__(self): + raise StopAsyncIteration + + def map(self, *args, **kwargs): + return self + + async def get_final_response(self): + return None + + stream = _Stream() + + def _fil_get_response(self, *args, **kwargs): + return stream + + class _FunctionInvocationLayer: + get_response = staticmethod(_fil_get_response) + + class _ChatTelemetryLayer: + get_response = staticmethod(lambda *a, **kw: None) + + tools_mod = types.ModuleType("agent_framework._tools") + tools_mod.FunctionInvocationLayer = _FunctionInvocationLayer # type: ignore[attr-defined] + obs_mod = types.ModuleType("agent_framework.observability") + obs_mod.ChatTelemetryLayer = _ChatTelemetryLayer # type: ignore[attr-defined] + af_mod = types.ModuleType("agent_framework") + af_mod._tools = tools_mod # type: ignore[attr-defined] + af_mod.observability = obs_mod # type: ignore[attr-defined] + + monkeypatch.setitem(sys.modules, "agent_framework", af_mod) + monkeypatch.setitem(sys.modules, "agent_framework._tools", tools_mod) + monkeypatch.setitem(sys.modules, "agent_framework.observability", obs_mod) + + react_step_patch.revert_react_step_patch() + react_step_patch._applied = False + react_step_patch._original_fil_get_response = None + react_step_patch._original_chat_get_response = None + _reset_extended_handler_singletons() + + react_step_patch.apply_react_step_patch(tracer_provider=None) + result = _FunctionInvocationLayer.get_response("self-arg", stream=True) + assert result is stream + assert not asyncio.iscoroutine(result) + + react_step_patch.revert_react_step_patch() + + def test_chat_wrapper_outside_loop_passes_through(monkeypatch): """[P0] Outside a react-loop scope, the chat wrapper must return the raw coroutine produced by the wrapped function (no react_step span). This preserves the normal ``await layer.get_response(...)`` path used by MAF when ReAct is not active. """ - import asyncio import sys import types @@ -275,18 +374,71 @@ class _ChatTelemetryLayer: monkeypatch.setitem(sys.modules, "agent_framework.observability", obs_mod) react_step_patch.revert_react_step_patch() + + +def test_chat_wrapper_inside_loop_preserves_streaming_response_type( + monkeypatch, +): + """Inside the ReAct ContextVar scope, a streaming ResponseStream-like value + must still pass through unchanged. ReAct step spans are only added around + awaitable non-streaming chat calls. + """ + import asyncio + import sys + import types + + class _Stream: + def __await__(self): + async def _done(): + return self + + return _done().__await__() + + def __aiter__(self): + return self + + async def __anext__(self): + raise StopAsyncIteration + + def map(self, *args, **kwargs): + return self + + async def get_final_response(self): + return None + + stream = _Stream() + + class _FunctionInvocationLayer: + get_response = staticmethod(lambda *a, **kw: None) + + class _ChatTelemetryLayer: + get_response = staticmethod(lambda *a, **kw: stream) + + tools_mod = types.ModuleType("agent_framework._tools") + tools_mod.FunctionInvocationLayer = _FunctionInvocationLayer # type: ignore[attr-defined] + obs_mod = types.ModuleType("agent_framework.observability") + obs_mod.ChatTelemetryLayer = _ChatTelemetryLayer # type: ignore[attr-defined] + af_mod = types.ModuleType("agent_framework") + af_mod._tools = tools_mod # type: ignore[attr-defined] + af_mod.observability = obs_mod # type: ignore[attr-defined] + + monkeypatch.setitem(sys.modules, "agent_framework", af_mod) + monkeypatch.setitem(sys.modules, "agent_framework._tools", tools_mod) + monkeypatch.setitem(sys.modules, "agent_framework.observability", obs_mod) + + react_step_patch.revert_react_step_patch() react_step_patch._applied = False react_step_patch._original_fil_get_response = None react_step_patch._original_chat_get_response = None - react_step_patch._handler = None + _reset_extended_handler_singletons() react_step_patch.apply_react_step_patch(tracer_provider=None) - - coro = _ChatTelemetryLayer.get_response("self-arg") - assert asyncio.iscoroutine(coro), ( - "Chat wrapper must pass through the wrapped coroutine unchanged" - ) - result = asyncio.get_event_loop().run_until_complete(coro) - assert result[0] == "chat-ok" + token = react_step_patch._maf_react_loop_active.set(True) + try: + result = _ChatTelemetryLayer.get_response("self-arg", stream=True) + assert result is stream + assert not asyncio.iscoroutine(result) + finally: + react_step_patch._maf_react_loop_active.reset(token) react_step_patch.revert_react_step_patch() diff --git a/pyproject.toml b/pyproject.toml index 4ed5f45ff..f175dd24c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -201,6 +201,9 @@ ignore = [ "instrumentation-loongsuite/loongsuite-instrumentation-widesearch/tests/**/*.py" = ["E402", "F811"] "instrumentation-loongsuite/loongsuite-instrumentation-wildtool/**/*.py" = ["PLC0415"] "instrumentation-loongsuite/loongsuite-instrumentation-wildtool/tests/**/*.py" = ["E402", "F811"] +# Microsoft Agent Framework is an optional dependency and its internal modules +# are imported lazily to keep auto-instrumentation usable when MAF is absent. +"instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/**/*.py" = ["PLC0415"] [tool.ruff.lint.isort] detect-same-package = false # to not consider instrumentation packages as first-party diff --git a/scripts/generate_instrumentation_bootstrap.py b/scripts/generate_instrumentation_bootstrap.py index dd40e0223..3b925856e 100755 --- a/scripts/generate_instrumentation_bootstrap.py +++ b/scripts/generate_instrumentation_bootstrap.py @@ -84,6 +84,11 @@ # development. This filter will get removed once it is further along in its # development lifecycle and ready to be included by default. "opentelemetry-instrumentation-claude-agent-sdk", + # Microsoft Agent Framework instrumentation is currently excluded because + # the official framework package requires a newer OpenTelemetry API than + # this workspace line. Users should install agent-framework-core in their + # application environment explicitly. + "opentelemetry-instrumentation-microsoft-agent-framework", ] # Static version specifiers for instrumentations that are released independently diff --git a/uv.lock b/uv.lock index 5f486e1a6..94803e635 100644 --- a/uv.lock +++ b/uv.lock @@ -46,6 +46,7 @@ members = [ "opentelemetry-instrumentation-kafka-python", "opentelemetry-instrumentation-langchain", "opentelemetry-instrumentation-logging", + "opentelemetry-instrumentation-microsoft-agent-framework", "opentelemetry-instrumentation-mysql", "opentelemetry-instrumentation-mysqlclient", "opentelemetry-instrumentation-openai-agents-v2", @@ -3136,6 +3137,27 @@ requires-dist = [ ] provides-extras = ["instruments"] +[[package]] +name = "opentelemetry-instrumentation-microsoft-agent-framework" +source = { editable = "instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-genai" }, + { name = "wrapt" }, +] + +[package.metadata] +requires-dist = [ + { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, + { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, + { name = "opentelemetry-util-genai", editable = "util/opentelemetry-util-genai" }, + { name = "wrapt", specifier = ">=1.14" }, +] +provides-extras = ["instruments"] + [[package]] name = "opentelemetry-instrumentation-mysql" source = { editable = "instrumentation/opentelemetry-instrumentation-mysql" } From 4be6a790fbf643ed9b76e7aa1c0ebbd3684932a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=81=E5=B1=BF?= Date: Thu, 25 Jun 2026 20:22:35 +0800 Subject: [PATCH 07/21] fix(maf): address review comments and ci --- .../CHANGELOG.md | 13 +++++ .../README.rst | 2 +- .../microsoft_agent_framework/config.py | 8 ++- .../semantic_conventions.py | 10 ++-- .../span_processor.py | 54 +++++++++++++----- .../tests/test_processor.py | 55 +++++++++++++------ 6 files changed, 102 insertions(+), 40 deletions(-) create mode 100644 instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/CHANGELOG.md diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/CHANGELOG.md b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/CHANGELOG.md new file mode 100644 index 000000000..304e36938 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased + +### Added + +- Add Microsoft Agent Framework instrumentation aligned with LoongSuite GenAI semantic conventions. + ([#229](https://github.com/alibaba/loongsuite-python/pull/229)) diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/README.rst b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/README.rst index 9d3337a4b..b101bb54c 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/README.rst +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/README.rst @@ -42,7 +42,7 @@ The framework dependency is intentionally not included in the package's ``1.37`` OpenTelemetry API line. Configuration -============ +============= ====================================== ========== ============================================================== Env Default Description diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/config.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/config.py index af65bf71b..7d5526886 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/config.py +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/config.py @@ -27,11 +27,15 @@ def _resolve_bool(value: Any, default: bool) -> bool: def is_instrumentation_enabled(default: bool = True) -> bool: - return _resolve_bool(os.getenv(ENV_INSTRUMENTATION_ENABLED), default=default) + return _resolve_bool( + os.getenv(ENV_INSTRUMENTATION_ENABLED), default=default + ) def is_sensitive_data_enabled(default: bool = False) -> bool: - return _resolve_bool(os.getenv(ENV_SENSITIVE_DATA_ENABLED), default=default) + return _resolve_bool( + os.getenv(ENV_SENSITIVE_DATA_ENABLED), default=default + ) def is_react_step_enabled(default: bool = False) -> bool: diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py index c1b8d20d6..9c07b757f 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py @@ -96,13 +96,14 @@ class GenAIOperation: "message.destination_executor_id": "gen_ai.message.destination_executor_id", } -# Provider name normalization — collapse MAF-specific provider spellings to the -# canonical OTel/ARMS value to avoid dimension sprawl in metrics. +# Provider name normalization — collapse known aliases to the canonical OTel/ARMS +# value to avoid dimension sprawl in metrics. Framework names such as +# ``microsoft.agent_framework`` are intentionally not mapped to a concrete model +# provider because MAF can route to different underlying providers. PROVIDER_NAME_NORMALIZE: Final[dict[str, str]] = { "azure_openai": "openai", "azure_ai_openai": "openai", "azure.openai": "openai", - "microsoft.agent_framework": "openai", } # Attribute keys we read off the span. Centralized so tests can import them. @@ -117,7 +118,4 @@ class GenAIOperation: GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens" GEN_AI_REACT_ROUND = "gen_ai.react.round" GEN_AI_REACT_FINISH_REASON = "gen_ai.react.finish_reason" -GEN_AI_FRAMEWORK = "gen_ai.framework" ERROR_TYPE = "error.type" - -FRAMEWORK_NAME = "microsoft-agent-framework" diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py index 2046fb5e4..a3693f171 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py @@ -29,6 +29,7 @@ import json import logging import threading +import time from collections import defaultdict from typing import Any, Dict, Optional, Tuple @@ -75,6 +76,7 @@ _MESSAGE_SEND = "message.send" _EXECUTOR_PROCESS = "executor.process" _EDGE_GROUP_PROCESS = "edge_group.process" +_LIVE_SPAN_MAX_AGE_NS = 60 * 1_000_000_000 def _attr_value(span: Any, key: str) -> Any: @@ -217,16 +219,15 @@ def _rename_maf_attrs(live_span: OtelSpan, readable: Any) -> list[str]: def _normalize_provider(value: Any) -> Optional[str]: """Normalize ``gen_ai.provider.name`` to the ARMS canonical value. - MAF writes a few variants (``azure_openai``, ``microsoft.agent_framework``, - different casing on AGENT vs LLM spans, or wraps the value in a sequence - for some span types). We: + MAF can write OpenAI aliases or framework-level values, and may wrap the + value in a sequence for some span types. We: 1. Unwrap sequence attribute values (OTel allows ``str | sequence[str]``). 2. Try an exact match against ``PROVIDER_NAME_NORMALIZE``. - 3. Fall back to a case-insensitive match — MAF emits - ``microsoft.agent_framework`` on AGENT spans in a slightly different - spelling than the LLM span's ``openai``, and we want both to collapse - to the same dimension regardless of casing. + 3. Fall back to a case-insensitive match. + 4. Return the lower-cased raw value for unknown providers. We intentionally + do not map ``microsoft.agent_framework`` to ``openai`` because MAF can + route to multiple underlying model providers. """ if value is None: return None @@ -241,7 +242,7 @@ def _normalize_provider(value: Any) -> Optional[str]: lowered = value.lower() if lowered in PROVIDER_NAME_NORMALIZE: return PROVIDER_NAME_NORMALIZE[lowered] - return value + return lowered def _normalize_finish_reasons(live_span: OtelSpan, readable: Any) -> None: @@ -466,6 +467,7 @@ def __init__( ) -> None: self._live_spans: Dict[str, OtelSpan] = {} self._span_parents: Dict[str, Optional[str]] = {} + self._live_span_lock = threading.Lock() self._slow_threshold_ns = int(slow_threshold_ms) * 1_000_000 self._capture_sensitive = capture_sensitive_data self._counters = _Counters() @@ -656,7 +658,6 @@ def on_start( key = format(sid, "016x") except Exception: return - self._live_spans[key] = span parent = getattr(span, "_parent", None) parent_id = None if parent is not None: @@ -664,7 +665,9 @@ def on_start( parent_id = format(parent.span_id, "016x") except Exception: parent_id = None - self._span_parents[key] = parent_id + with self._live_span_lock: + self._live_spans[key] = span + self._span_parents[key] = parent_id def on_end(self, span: Any) -> None: """Enrich a just-ended MAF span with ARMS GenAI semantic conventions.""" @@ -673,8 +676,9 @@ def on_end(self, span: Any) -> None: key = format(ctx.span_id, "016x") except Exception: return - live = self._live_spans.pop(key, None) - parent_id = self._span_parents.pop(key, None) + with self._live_span_lock: + live = self._live_spans.pop(key, None) + parent_id = self._span_parents.pop(key, None) if live is None: return # NOTE: by the time on_end runs, the SDK has already called Span.end(), @@ -819,12 +823,34 @@ def _aggregate_metrics( logger.debug("MAF metrics aggregation failed: %s", exc) def shutdown(self) -> None: - self._live_spans.clear() - self._span_parents.clear() + with self._live_span_lock: + self._live_spans.clear() + self._span_parents.clear() def force_flush(self, timeout_millis: int = 30000) -> bool: + self._sweep_stale_live_spans() return True + def _sweep_stale_live_spans( + self, max_age_ns: int = _LIVE_SPAN_MAX_AGE_NS + ) -> None: + """Bound live-span bookkeeping if a span is started but never ended.""" + now_ns = time.time_ns() + stale_keys = [] + with self._live_span_lock: + for key, live_span in list(self._live_spans.items()): + start_time = getattr(live_span, "start_time", None) + if start_time is None: + continue + try: + if now_ns - int(start_time) > max_age_ns: + stale_keys.append(key) + except (TypeError, ValueError): + continue + for key in stale_keys: + self._live_spans.pop(key, None) + self._span_parents.pop(key, None) + def _obs(value: float, attrs: Dict[str, str]): """Build an Observation compatible with OTel callbacks.""" diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_processor.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_processor.py index fdb4c06dd..4e534e98e 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_processor.py +++ b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_processor.py @@ -451,47 +451,68 @@ def test_mcp_span_op_name_overridden_to_mcp_when_maf_writes_execute_tool(): assert s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.MCP -def test_provider_normalization_microsoft_agent_framework_to_openai(): - """[P3] regression: AGENT spans written by MAF carry - ``gen_ai.provider.name=microsoft.agent_framework``. The - ``PROVIDER_NAME_NORMALIZE`` map must collapse this to ``openai`` so AGENT - spans share the same dimension as the LLM spans beneath them. Before the - fix, the AGENT span's provider stayed at the raw MAF value while LLM was - already ``openai``. +def test_provider_normalization_keeps_framework_provider_separate(): + """Framework-level provider names must not be collapsed to ``openai``. + + MAF can route to multiple underlying providers, so ``microsoft.agent_framework`` + is lower-cased and kept distinct instead of pretending every MAF span used + OpenAI. """ tp, tracer, exporter, _ = _setup() with tracer.start_as_current_span("invoke_agent my-agent") as span: span.set_attribute(GEN_AI_OPERATION_NAME, GenAIOperation.INVOKE_AGENT) span.set_attribute(GEN_AI_PROVIDER_NAME, "microsoft.agent_framework") spans = _flush(exporter) - assert spans[0].attributes.get(GEN_AI_PROVIDER_NAME) == "openai", ( - "AGENT provider.name should normalize from microsoft.agent_framework to openai" + assert ( + spans[0].attributes.get(GEN_AI_PROVIDER_NAME) + == "microsoft.agent_framework" ) def test_provider_normalization_case_insensitive_variant(): - """[P3] MAF may emit the provider value in different casing across span - types (AGENT vs LLM). Normalization should collapse to the same canonical - value regardless of case.""" + """Unknown provider values should lower-case to avoid metric cardinality.""" tp, tracer, exporter, _ = _setup() with tracer.start_as_current_span("invoke_agent my-agent") as span: span.set_attribute(GEN_AI_OPERATION_NAME, GenAIOperation.INVOKE_AGENT) span.set_attribute(GEN_AI_PROVIDER_NAME, "Microsoft.Agent_Framework") spans = _flush(exporter) - assert spans[0].attributes.get(GEN_AI_PROVIDER_NAME) == "openai" + assert ( + spans[0].attributes.get(GEN_AI_PROVIDER_NAME) + == "microsoft.agent_framework" + ) def test_provider_normalization_list_wrapped_value(): """[P3] OTel attributes may be a sequence of strings. MAF occasionally writes ``gen_ai.provider.name`` as ``["microsoft.agent_framework"]`` on - AGENT spans. The normalizer should unwrap the sequence and collapse the - first element.""" + AGENT spans. The normalizer should unwrap the sequence and normalize the + first element's casing.""" tp, tracer, exporter, _ = _setup() with tracer.start_as_current_span("invoke_agent my-agent") as span: span.set_attribute(GEN_AI_OPERATION_NAME, GenAIOperation.INVOKE_AGENT) - span.set_attribute(GEN_AI_PROVIDER_NAME, ["microsoft.agent_framework"]) + span.set_attribute(GEN_AI_PROVIDER_NAME, ["Microsoft.Agent_Framework"]) spans = _flush(exporter) - assert spans[0].attributes.get(GEN_AI_PROVIDER_NAME) == "openai" + assert ( + spans[0].attributes.get(GEN_AI_PROVIDER_NAME) + == "microsoft.agent_framework" + ) + + +def test_force_flush_sweeps_stale_live_spans(): + class _StartedSpan: + start_time = 0 + + processor = MAFSemanticProcessor( + meter_provider=None, + metrics_enabled=False, + capture_sensitive_data=False, + ) + processor._live_spans["deadbeef"] = _StartedSpan() + processor._span_parents["deadbeef"] = None + + assert processor.force_flush() + assert "deadbeef" not in processor._live_spans + assert "deadbeef" not in processor._span_parents def test_instrument_prepends_processor_before_existing_exporters(): From 29c3fdcda88ac9c0bbc8f772f45ad17e4a2ce7b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=81=E5=B1=BF?= Date: Fri, 26 Jun 2026 12:09:27 +0800 Subject: [PATCH 08/21] fix(maf): move instrumentation into loongsuite matrix --- .github/workflows/loongsuite_lint_0.yml | 2 +- instrumentation-genai/README.md | 1 - .../microsoft_agent_framework/version.py | 1 - .../CHANGELOG.md | 4 +++ .../README.rst | 24 ++++++++++-------- .../pyproject.toml | 22 ++++++++++------ .../microsoft_agent_framework/__init__.py | 0 .../microsoft_agent_framework/config.py | 0 .../microsoft_agent_framework/package.py | 0 .../microsoft_agent_framework/py.typed | 0 .../react_step_patch.py | 0 .../semantic_conventions.py | 0 .../span_processor.py | 0 .../microsoft_agent_framework/version.py | 1 + .../tests/__init__.py | 0 .../tests/requirements.latest.txt | 25 +++++++++++++++++++ .../tests/test_config.py | 0 .../tests/test_processor.py | 0 .../tests/test_react_step.py | 0 pyproject.toml | 2 +- scripts/generate_instrumentation_bootstrap.py | 5 ---- tox-loongsuite.ini | 12 +++++++++ uv.lock | 22 ---------------- 23 files changed, 71 insertions(+), 50 deletions(-) delete mode 100644 instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/version.py rename {instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework => instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework}/CHANGELOG.md (84%) rename {instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework => instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework}/README.rst (78%) rename {instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework => instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework}/pyproject.toml (62%) rename {instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework => instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework}/src/opentelemetry/instrumentation/microsoft_agent_framework/__init__.py (100%) rename {instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework => instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework}/src/opentelemetry/instrumentation/microsoft_agent_framework/config.py (100%) rename {instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework => instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework}/src/opentelemetry/instrumentation/microsoft_agent_framework/package.py (100%) rename {instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework => instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework}/src/opentelemetry/instrumentation/microsoft_agent_framework/py.typed (100%) rename {instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework => instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework}/src/opentelemetry/instrumentation/microsoft_agent_framework/react_step_patch.py (100%) rename {instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework => instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework}/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py (100%) rename {instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework => instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework}/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py (100%) create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/version.py rename {instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework => instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework}/tests/__init__.py (100%) create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/requirements.latest.txt rename {instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework => instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework}/tests/test_config.py (100%) rename {instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework => instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework}/tests/test_processor.py (100%) rename {instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework => instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework}/tests/test_react_step.py (100%) diff --git a/.github/workflows/loongsuite_lint_0.yml b/.github/workflows/loongsuite_lint_0.yml index 3e9326cd3..8b51ece98 100644 --- a/.github/workflows/loongsuite_lint_0.yml +++ b/.github/workflows/loongsuite_lint_0.yml @@ -84,7 +84,7 @@ jobs: shell: bash env: LOONGSUITE_ALL_JOBS: >- - [{"name": "lint-loongsuite-instrumentation-agentscope", "package": "loongsuite-instrumentation-agentscope", "tox_env": "lint-loongsuite-instrumentation-agentscope", "ui_name": "loongsuite-instrumentation-agentscope"}, {"name": "lint-loongsuite-instrumentation-dashscope", "package": "loongsuite-instrumentation-dashscope", "tox_env": "lint-loongsuite-instrumentation-dashscope", "ui_name": "loongsuite-instrumentation-dashscope"}, {"name": "lint-loongsuite-instrumentation-claude-agent-sdk", "package": "loongsuite-instrumentation-claude-agent-sdk", "tox_env": "lint-loongsuite-instrumentation-claude-agent-sdk", "ui_name": "loongsuite-instrumentation-claude-agent-sdk"}, {"name": "lint-loongsuite-instrumentation-google-adk", "package": "loongsuite-instrumentation-google-adk", "tox_env": "lint-loongsuite-instrumentation-google-adk", "ui_name": "loongsuite-instrumentation-google-adk"}, {"name": "lint-loongsuite-instrumentation-agno", "package": "loongsuite-instrumentation-agno", "tox_env": "lint-loongsuite-instrumentation-agno", "ui_name": "loongsuite-instrumentation-agno"}, {"name": "lint-loongsuite-instrumentation-langchain", "package": "loongsuite-instrumentation-langchain", "tox_env": "lint-loongsuite-instrumentation-langchain", "ui_name": "loongsuite-instrumentation-langchain"}, {"name": "lint-loongsuite-instrumentation-langgraph", "package": "loongsuite-instrumentation-langgraph", "tox_env": "lint-loongsuite-instrumentation-langgraph", "ui_name": "loongsuite-instrumentation-langgraph"}, {"name": "lint-loongsuite-instrumentation-deepagents", "package": "loongsuite-instrumentation-deepagents", "tox_env": "lint-loongsuite-instrumentation-deepagents", "ui_name": "loongsuite-instrumentation-deepagents"}, {"name": "lint-loongsuite-instrumentation-qwen-agent", "package": "loongsuite-instrumentation-qwen-agent", "tox_env": "lint-loongsuite-instrumentation-qwen-agent", "ui_name": "loongsuite-instrumentation-qwen-agent"}, {"name": "lint-loongsuite-instrumentation-hermes-agent", "package": "loongsuite-instrumentation-hermes-agent", "tox_env": "lint-loongsuite-instrumentation-hermes-agent", "ui_name": "loongsuite-instrumentation-hermes-agent"}, {"name": "lint-loongsuite-instrumentation-mem0", "package": "loongsuite-instrumentation-mem0", "tox_env": "lint-loongsuite-instrumentation-mem0", "ui_name": "loongsuite-instrumentation-mem0"}, {"name": "lint-util-genai", "package": "util-genai", "tox_env": "lint-util-genai", "ui_name": "util-genai"}, {"name": "lint-loongsuite-instrumentation-litellm", "package": "loongsuite-instrumentation-litellm", "tox_env": "lint-loongsuite-instrumentation-litellm", "ui_name": "loongsuite-instrumentation-litellm"}, {"name": "lint-loongsuite-instrumentation-crewai", "package": "loongsuite-instrumentation-crewai", "tox_env": "lint-loongsuite-instrumentation-crewai", "ui_name": "loongsuite-instrumentation-crewai"}, {"name": "lint-loongsuite-instrumentation-qwenpaw", "package": "loongsuite-instrumentation-qwenpaw", "tox_env": "lint-loongsuite-instrumentation-qwenpaw", "ui_name": "loongsuite-instrumentation-qwenpaw"}, {"name": "lint-loongsuite-instrumentation-algotune", "package": "loongsuite-instrumentation-algotune", "tox_env": "lint-loongsuite-instrumentation-algotune", "ui_name": "loongsuite-instrumentation-algotune"}, {"name": "lint-loongsuite-instrumentation-bfclv4", "package": "loongsuite-instrumentation-bfclv4", "tox_env": "lint-loongsuite-instrumentation-bfclv4", "ui_name": "loongsuite-instrumentation-bfclv4"}, {"name": "lint-loongsuite-instrumentation-claw-eval", "package": "loongsuite-instrumentation-claw-eval", "tox_env": "lint-loongsuite-instrumentation-claw-eval", "ui_name": "loongsuite-instrumentation-claw-eval"}, {"name": "lint-loongsuite-instrumentation-minisweagent", "package": "loongsuite-instrumentation-minisweagent", "tox_env": "lint-loongsuite-instrumentation-minisweagent", "ui_name": "loongsuite-instrumentation-minisweagent"}, {"name": "lint-loongsuite-instrumentation-openhands", "package": "loongsuite-instrumentation-openhands", "tox_env": "lint-loongsuite-instrumentation-openhands", "ui_name": "loongsuite-instrumentation-openhands"}, {"name": "lint-loongsuite-instrumentation-slop-code", "package": "loongsuite-instrumentation-slop-code", "tox_env": "lint-loongsuite-instrumentation-slop-code", "ui_name": "loongsuite-instrumentation-slop-code"}, {"name": "lint-loongsuite-instrumentation-terminus2", "package": "loongsuite-instrumentation-terminus2", "tox_env": "lint-loongsuite-instrumentation-terminus2", "ui_name": "loongsuite-instrumentation-terminus2"}, {"name": "lint-loongsuite-instrumentation-vita", "package": "loongsuite-instrumentation-vita", "tox_env": "lint-loongsuite-instrumentation-vita", "ui_name": "loongsuite-instrumentation-vita"}, {"name": "lint-loongsuite-instrumentation-webarena", "package": "loongsuite-instrumentation-webarena", "tox_env": "lint-loongsuite-instrumentation-webarena", "ui_name": "loongsuite-instrumentation-webarena"}, {"name": "lint-loongsuite-instrumentation-widesearch", "package": "loongsuite-instrumentation-widesearch", "tox_env": "lint-loongsuite-instrumentation-widesearch", "ui_name": "loongsuite-instrumentation-widesearch"}, {"name": "lint-loongsuite-instrumentation-wildtool", "package": "loongsuite-instrumentation-wildtool", "tox_env": "lint-loongsuite-instrumentation-wildtool", "ui_name": "loongsuite-instrumentation-wildtool"}] + [{"name": "lint-loongsuite-instrumentation-agentscope", "package": "loongsuite-instrumentation-agentscope", "tox_env": "lint-loongsuite-instrumentation-agentscope", "ui_name": "loongsuite-instrumentation-agentscope"}, {"name": "lint-loongsuite-instrumentation-dashscope", "package": "loongsuite-instrumentation-dashscope", "tox_env": "lint-loongsuite-instrumentation-dashscope", "ui_name": "loongsuite-instrumentation-dashscope"}, {"name": "lint-loongsuite-instrumentation-claude-agent-sdk", "package": "loongsuite-instrumentation-claude-agent-sdk", "tox_env": "lint-loongsuite-instrumentation-claude-agent-sdk", "ui_name": "loongsuite-instrumentation-claude-agent-sdk"}, {"name": "lint-loongsuite-instrumentation-google-adk", "package": "loongsuite-instrumentation-google-adk", "tox_env": "lint-loongsuite-instrumentation-google-adk", "ui_name": "loongsuite-instrumentation-google-adk"}, {"name": "lint-loongsuite-instrumentation-agno", "package": "loongsuite-instrumentation-agno", "tox_env": "lint-loongsuite-instrumentation-agno", "ui_name": "loongsuite-instrumentation-agno"}, {"name": "lint-loongsuite-instrumentation-langchain", "package": "loongsuite-instrumentation-langchain", "tox_env": "lint-loongsuite-instrumentation-langchain", "ui_name": "loongsuite-instrumentation-langchain"}, {"name": "lint-loongsuite-instrumentation-langgraph", "package": "loongsuite-instrumentation-langgraph", "tox_env": "lint-loongsuite-instrumentation-langgraph", "ui_name": "loongsuite-instrumentation-langgraph"}, {"name": "lint-loongsuite-instrumentation-deepagents", "package": "loongsuite-instrumentation-deepagents", "tox_env": "lint-loongsuite-instrumentation-deepagents", "ui_name": "loongsuite-instrumentation-deepagents"}, {"name": "lint-loongsuite-instrumentation-microsoft-agent-framework", "package": "loongsuite-instrumentation-microsoft-agent-framework", "tox_env": "lint-loongsuite-instrumentation-microsoft-agent-framework", "ui_name": "loongsuite-instrumentation-microsoft-agent-framework"}, {"name": "lint-loongsuite-instrumentation-qwen-agent", "package": "loongsuite-instrumentation-qwen-agent", "tox_env": "lint-loongsuite-instrumentation-qwen-agent", "ui_name": "loongsuite-instrumentation-qwen-agent"}, {"name": "lint-loongsuite-instrumentation-hermes-agent", "package": "loongsuite-instrumentation-hermes-agent", "tox_env": "lint-loongsuite-instrumentation-hermes-agent", "ui_name": "loongsuite-instrumentation-hermes-agent"}, {"name": "lint-loongsuite-instrumentation-mem0", "package": "loongsuite-instrumentation-mem0", "tox_env": "lint-loongsuite-instrumentation-mem0", "ui_name": "loongsuite-instrumentation-mem0"}, {"name": "lint-util-genai", "package": "util-genai", "tox_env": "lint-util-genai", "ui_name": "util-genai"}, {"name": "lint-loongsuite-instrumentation-litellm", "package": "loongsuite-instrumentation-litellm", "tox_env": "lint-loongsuite-instrumentation-litellm", "ui_name": "loongsuite-instrumentation-litellm"}, {"name": "lint-loongsuite-instrumentation-crewai", "package": "loongsuite-instrumentation-crewai", "tox_env": "lint-loongsuite-instrumentation-crewai", "ui_name": "loongsuite-instrumentation-crewai"}, {"name": "lint-loongsuite-instrumentation-qwenpaw", "package": "loongsuite-instrumentation-qwenpaw", "tox_env": "lint-loongsuite-instrumentation-qwenpaw", "ui_name": "loongsuite-instrumentation-qwenpaw"}, {"name": "lint-loongsuite-instrumentation-algotune", "package": "loongsuite-instrumentation-algotune", "tox_env": "lint-loongsuite-instrumentation-algotune", "ui_name": "loongsuite-instrumentation-algotune"}, {"name": "lint-loongsuite-instrumentation-bfclv4", "package": "loongsuite-instrumentation-bfclv4", "tox_env": "lint-loongsuite-instrumentation-bfclv4", "ui_name": "loongsuite-instrumentation-bfclv4"}, {"name": "lint-loongsuite-instrumentation-claw-eval", "package": "loongsuite-instrumentation-claw-eval", "tox_env": "lint-loongsuite-instrumentation-claw-eval", "ui_name": "loongsuite-instrumentation-claw-eval"}, {"name": "lint-loongsuite-instrumentation-minisweagent", "package": "loongsuite-instrumentation-minisweagent", "tox_env": "lint-loongsuite-instrumentation-minisweagent", "ui_name": "loongsuite-instrumentation-minisweagent"}, {"name": "lint-loongsuite-instrumentation-openhands", "package": "loongsuite-instrumentation-openhands", "tox_env": "lint-loongsuite-instrumentation-openhands", "ui_name": "loongsuite-instrumentation-openhands"}, {"name": "lint-loongsuite-instrumentation-slop-code", "package": "loongsuite-instrumentation-slop-code", "tox_env": "lint-loongsuite-instrumentation-slop-code", "ui_name": "loongsuite-instrumentation-slop-code"}, {"name": "lint-loongsuite-instrumentation-terminus2", "package": "loongsuite-instrumentation-terminus2", "tox_env": "lint-loongsuite-instrumentation-terminus2", "ui_name": "loongsuite-instrumentation-terminus2"}, {"name": "lint-loongsuite-instrumentation-vita", "package": "loongsuite-instrumentation-vita", "tox_env": "lint-loongsuite-instrumentation-vita", "ui_name": "loongsuite-instrumentation-vita"}, {"name": "lint-loongsuite-instrumentation-webarena", "package": "loongsuite-instrumentation-webarena", "tox_env": "lint-loongsuite-instrumentation-webarena", "ui_name": "loongsuite-instrumentation-webarena"}, {"name": "lint-loongsuite-instrumentation-widesearch", "package": "loongsuite-instrumentation-widesearch", "tox_env": "lint-loongsuite-instrumentation-widesearch", "ui_name": "loongsuite-instrumentation-widesearch"}, {"name": "lint-loongsuite-instrumentation-wildtool", "package": "loongsuite-instrumentation-wildtool", "tox_env": "lint-loongsuite-instrumentation-wildtool", "ui_name": "loongsuite-instrumentation-wildtool"}] LOONGSUITE_FULL: ${{ steps.detect.outputs.full }} LOONGSUITE_PACKAGES: ${{ steps.detect.outputs.packages }} run: | diff --git a/instrumentation-genai/README.md b/instrumentation-genai/README.md index faca91aac..dbe51b523 100644 --- a/instrumentation-genai/README.md +++ b/instrumentation-genai/README.md @@ -5,7 +5,6 @@ | [opentelemetry-instrumentation-claude-agent-sdk](./opentelemetry-instrumentation-claude-agent-sdk) | claude-agent-sdk >= 0.1.14 | No | development | [opentelemetry-instrumentation-google-genai](./opentelemetry-instrumentation-google-genai) | google-genai >= 1.32.0 | No | development | [opentelemetry-instrumentation-langchain](./opentelemetry-instrumentation-langchain) | langchain >= 0.3.21 | No | development -| [opentelemetry-instrumentation-microsoft-agent-framework](./opentelemetry-instrumentation-microsoft-agent-framework) | agent-framework-core >= 1.0.0 | Yes | development | [opentelemetry-instrumentation-openai-agents-v2](./opentelemetry-instrumentation-openai-agents-v2) | openai-agents >= 0.3.3 | No | development | [opentelemetry-instrumentation-openai-v2](./opentelemetry-instrumentation-openai-v2) | openai >= 1.26.0 | Yes | development | [opentelemetry-instrumentation-vertexai](./opentelemetry-instrumentation-vertexai) | google-cloud-aiplatform >= 1.64 | No | development diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/version.py b/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/version.py deleted file mode 100644 index a6645841e..000000000 --- a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "0.1.0.dev" diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/CHANGELOG.md b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/CHANGELOG.md similarity index 84% rename from instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/CHANGELOG.md rename to instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/CHANGELOG.md index 304e36938..21d9c2c46 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/CHANGELOG.md +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/CHANGELOG.md @@ -11,3 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add Microsoft Agent Framework instrumentation aligned with LoongSuite GenAI semantic conventions. ([#229](https://github.com/alibaba/loongsuite-python/pull/229)) + +## Version 0.6.0 (2026-06-03) + +There are no changelog entries for this release. diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/README.rst b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/README.rst similarity index 78% rename from instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/README.rst rename to instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/README.rst index b101bb54c..8145ff529 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/README.rst +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/README.rst @@ -1,8 +1,8 @@ -===================================================== -Microsoft Agent Framework Instrumentation -===================================================== +======================================================== +LoongSuite Microsoft Agent Framework Instrumentation +======================================================== -This package provides OpenTelemetry instrumentation for +This package provides LoongSuite instrumentation for `Microsoft Agent Framework `_ (``agent-framework-core``). @@ -23,7 +23,7 @@ described in ``llm-dev/microsoft-agent-framework/investigate/execute.md``: Truncation / PII helpers are reused from ``opentelemetry.util.genai.utils`` (``gen_ai_json_dumps``), aligned with -``instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/.../span_processor.py``. +the OpenAI Agents v2 GenAI instrumentation. Installation ============ @@ -33,13 +33,15 @@ the target application environment: .. code-block:: console - pip install opentelemetry-instrumentation-microsoft-agent-framework - pip install agent-framework-core + pip install "loongsuite-instrumentation-microsoft-agent-framework[instruments]" + +To keep the framework dependency controlled by the application, install the +instrumentation package and framework separately: -The framework dependency is intentionally not included in the package's -``instruments`` extra because current ``agent-framework-core`` releases require -``opentelemetry-api>=1.39`` while this LoongSuite workspace still builds on the -``1.37`` OpenTelemetry API line. +.. code-block:: console + + pip install loongsuite-instrumentation-microsoft-agent-framework + pip install agent-framework-core Configuration ============= diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/pyproject.toml b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/pyproject.toml similarity index 62% rename from instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/pyproject.toml rename to instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/pyproject.toml index 525002ba2..ff969293a 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/pyproject.toml +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/pyproject.toml @@ -3,14 +3,15 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "opentelemetry-instrumentation-microsoft-agent-framework" +name = "loongsuite-instrumentation-microsoft-agent-framework" dynamic = ["version"] -description = "OpenTelemetry instrumentation for Microsoft Agent Framework (agent-framework-core)" +description = "LoongSuite Microsoft Agent Framework instrumentation" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ - { name = "ARMS LoongSuite Authors" }, + { name = "LoongSuite Python Agent Authors", email = "caishipeng.csp@alibaba-inc.com" }, + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] classifiers = [ "Development Status :: 4 - Beta", @@ -24,22 +25,24 @@ classifiers = [ "Programming Language :: Python :: 3.13", ] dependencies = [ - "opentelemetry-api >= 1.37", + "opentelemetry-api ~= 1.37", "opentelemetry-instrumentation >= 0.58b0", "opentelemetry-semantic-conventions >= 0.58b0", "opentelemetry-util-genai", - "wrapt >= 1.14", + "wrapt >= 1.14, < 2.0.0", ] [project.optional-dependencies] -instruments = [] +instruments = [ + "agent-framework-core >= 1.0.0", +] [project.entry-points.opentelemetry_instrumentor] microsoft_agent_framework = "opentelemetry.instrumentation.microsoft_agent_framework:MicrosoftAgentFrameworkInstrumentor" [project.urls] -Homepage = "https://github.com/alibaba/loongsuite-python-agent" -Repository = "https://github.com/alibaba/loongsuite-python-agent" +Homepage = "https://github.com/alibaba/loongsuite-python/tree/main/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework" +Repository = "https://github.com/alibaba/loongsuite-python" [tool.hatch.version] path = "src/opentelemetry/instrumentation/microsoft_agent_framework/version.py" @@ -53,3 +56,6 @@ include = [ [tool.hatch.build.targets.wheel] packages = ["src/opentelemetry"] + +[tool.loongsuite.readme] +supported-packages = ["agent-framework-core >= 1.0.0"] diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/__init__.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/__init__.py similarity index 100% rename from instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/__init__.py rename to instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/__init__.py diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/config.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/config.py similarity index 100% rename from instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/config.py rename to instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/config.py diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/package.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/package.py similarity index 100% rename from instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/package.py rename to instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/package.py diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/py.typed b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/py.typed similarity index 100% rename from instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/py.typed rename to instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/py.typed diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/react_step_patch.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/react_step_patch.py similarity index 100% rename from instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/react_step_patch.py rename to instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/react_step_patch.py diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py similarity index 100% rename from instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py rename to instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py similarity index 100% rename from instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py rename to instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/version.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/version.py new file mode 100644 index 000000000..08594a547 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/version.py @@ -0,0 +1 @@ +__version__ = "0.7.0.dev" diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/__init__.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/__init__.py similarity index 100% rename from instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/__init__.py rename to instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/__init__.py diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/requirements.latest.txt b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/requirements.latest.txt new file mode 100644 index 000000000..dbf0eb66f --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/requirements.latest.txt @@ -0,0 +1,25 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Installed together with {[testenv]test_deps} from tox-loongsuite.ini +# (opentelemetry-api/sdk/semantic-conventions from CORE_REPO). + +pytest==7.4.4 +pytest-asyncio==0.21.0 +wrapt>=1.14,<2.0.0 +opentelemetry-exporter-otlp-proto-http + +-e opentelemetry-instrumentation +-e util/opentelemetry-util-genai +-e instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_config.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_config.py similarity index 100% rename from instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_config.py rename to instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_config.py diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_processor.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_processor.py similarity index 100% rename from instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_processor.py rename to instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_processor.py diff --git a/instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_react_step.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_react_step.py similarity index 100% rename from instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/tests/test_react_step.py rename to instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_react_step.py diff --git a/pyproject.toml b/pyproject.toml index f175dd24c..ecd8c7fa6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -203,7 +203,7 @@ ignore = [ "instrumentation-loongsuite/loongsuite-instrumentation-wildtool/tests/**/*.py" = ["E402", "F811"] # Microsoft Agent Framework is an optional dependency and its internal modules # are imported lazily to keep auto-instrumentation usable when MAF is absent. -"instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework/**/*.py" = ["PLC0415"] +"instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/**/*.py" = ["PLC0415"] [tool.ruff.lint.isort] detect-same-package = false # to not consider instrumentation packages as first-party diff --git a/scripts/generate_instrumentation_bootstrap.py b/scripts/generate_instrumentation_bootstrap.py index 3b925856e..dd40e0223 100755 --- a/scripts/generate_instrumentation_bootstrap.py +++ b/scripts/generate_instrumentation_bootstrap.py @@ -84,11 +84,6 @@ # development. This filter will get removed once it is further along in its # development lifecycle and ready to be included by default. "opentelemetry-instrumentation-claude-agent-sdk", - # Microsoft Agent Framework instrumentation is currently excluded because - # the official framework package requires a newer OpenTelemetry API than - # this workspace line. Users should install agent-framework-core in their - # application environment explicitly. - "opentelemetry-instrumentation-microsoft-agent-framework", ] # Static version specifiers for instrumentations that are released independently diff --git a/tox-loongsuite.ini b/tox-loongsuite.ini index a7d561795..395e7fccc 100644 --- a/tox-loongsuite.ini +++ b/tox-loongsuite.ini @@ -48,6 +48,10 @@ envlist = py3{11,12,13}-test-loongsuite-instrumentation-deepagents-latest lint-loongsuite-instrumentation-deepagents + ; loongsuite-instrumentation-microsoft-agent-framework + py3{10,11,12,13}-test-loongsuite-instrumentation-microsoft-agent-framework + lint-loongsuite-instrumentation-microsoft-agent-framework + ; loongsuite-instrumentation-qwen-agent py3{9,10,11,12,13}-test-loongsuite-instrumentation-qwen-agent-{oldest,latest} lint-loongsuite-instrumentation-qwen-agent @@ -186,6 +190,11 @@ deps = lint-loongsuite-instrumentation-deepagents: {[testenv]test_deps} lint-loongsuite-instrumentation-deepagents: -r {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/requirements.latest.txt + microsoft-agent-framework: {[testenv]test_deps} + microsoft-agent-framework: -r {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/requirements.latest.txt + lint-loongsuite-instrumentation-microsoft-agent-framework: {[testenv]test_deps} + lint-loongsuite-instrumentation-microsoft-agent-framework: -r {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/requirements.latest.txt + qwen-agent-oldest: -r {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-qwen-agent/tests/requirements.oldest.txt qwen-agent-latest: {[testenv]test_deps} qwen-agent-latest: -r {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-qwen-agent/tests/requirements.latest.txt @@ -296,6 +305,9 @@ commands = test-loongsuite-instrumentation-deepagents: pytest {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests {posargs} lint-loongsuite-instrumentation-deepagents: python -m ruff check {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-deepagents + test-loongsuite-instrumentation-microsoft-agent-framework: pytest {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests {posargs} + lint-loongsuite-instrumentation-microsoft-agent-framework: python -m ruff check {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework + test-loongsuite-instrumentation-qwen-agent: pytest {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-qwen-agent/tests {posargs} lint-loongsuite-instrumentation-qwen-agent: python -m ruff check {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-qwen-agent diff --git a/uv.lock b/uv.lock index 94803e635..5f486e1a6 100644 --- a/uv.lock +++ b/uv.lock @@ -46,7 +46,6 @@ members = [ "opentelemetry-instrumentation-kafka-python", "opentelemetry-instrumentation-langchain", "opentelemetry-instrumentation-logging", - "opentelemetry-instrumentation-microsoft-agent-framework", "opentelemetry-instrumentation-mysql", "opentelemetry-instrumentation-mysqlclient", "opentelemetry-instrumentation-openai-agents-v2", @@ -3137,27 +3136,6 @@ requires-dist = [ ] provides-extras = ["instruments"] -[[package]] -name = "opentelemetry-instrumentation-microsoft-agent-framework" -source = { editable = "instrumentation-genai/opentelemetry-instrumentation-microsoft-agent-framework" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-genai" }, - { name = "wrapt" }, -] - -[package.metadata] -requires-dist = [ - { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, - { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, - { name = "opentelemetry-util-genai", editable = "util/opentelemetry-util-genai" }, - { name = "wrapt", specifier = ">=1.14" }, -] -provides-extras = ["instruments"] - [[package]] name = "opentelemetry-instrumentation-mysql" source = { editable = "instrumentation/opentelemetry-instrumentation-mysql" } From db33cac3b1669de2b8e863df42a3b4d8a11a973a Mon Sep 17 00:00:00 2001 From: Huxing Zhang Date: Sun, 28 Jun 2026 15:16:01 +0000 Subject: [PATCH 09/21] fix: add missing Apache 2.0 license headers Add license header to all 11 files in the microsoft-agent-framework instrumentation package. Resolves the check-license-header CI failure. --- .../microsoft_agent_framework/__init__.py | 14 ++++++++++++++ .../microsoft_agent_framework/config.py | 14 ++++++++++++++ .../microsoft_agent_framework/package.py | 14 ++++++++++++++ .../microsoft_agent_framework/react_step_patch.py | 14 ++++++++++++++ .../semantic_conventions.py | 14 ++++++++++++++ .../microsoft_agent_framework/span_processor.py | 14 ++++++++++++++ .../microsoft_agent_framework/version.py | 14 ++++++++++++++ .../tests/__init__.py | 14 ++++++++++++++ .../tests/test_config.py | 14 ++++++++++++++ .../tests/test_processor.py | 14 ++++++++++++++ .../tests/test_react_step.py | 14 ++++++++++++++ 11 files changed, 154 insertions(+) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/__init__.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/__init__.py index 1cedd57bc..7c682e3e3 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/__init__.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/__init__.py @@ -1,3 +1,17 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """OpenTelemetry instrumentation for Microsoft Agent Framework. This instrumentor enables MAF's built-in OTel telemetry (``enable_instrumentation`` diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/config.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/config.py index 7d5526886..b499300f6 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/config.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/config.py @@ -1,3 +1,17 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """Environment configuration for Microsoft Agent Framework instrumentation.""" from __future__ import annotations diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/package.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/package.py index 144e533ec..6b645b399 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/package.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/package.py @@ -1,2 +1,16 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + _instruments = ("agent-framework-core >= 1.0.0",) _supports_metrics = True diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/react_step_patch.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/react_step_patch.py index 7194ab5af..86cb2d81c 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/react_step_patch.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/react_step_patch.py @@ -1,3 +1,17 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """Optional ReAct step monkey patch for Microsoft Agent Framework. Emits one ``react step`` span per LLM round-trip inside the diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py index 9c07b757f..8dc7652d9 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py @@ -1,3 +1,17 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """Semantic convention constants and MAF→ARMS attribute mapping table. Centralizes: diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py index a3693f171..07eef3cfc 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py @@ -1,3 +1,17 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """``MAFSemanticProcessor`` — a ``SpanProcessor`` that enriches Microsoft Agent Framework's native OTel spans to align with the ARMS GenAI semantic conventions (``/apsara/semantic-conventions/arms_docs/trace/gen-ai.md``). diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/version.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/version.py index 08594a547..9fc24ea19 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/version.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/version.py @@ -1 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + __version__ = "0.7.0.dev" diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/__init__.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/__init__.py index e69de29bb..f87ce79b7 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/__init__.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/__init__.py @@ -0,0 +1,14 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_config.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_config.py index 166e0d77f..2cbe199d1 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_config.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_config.py @@ -1,3 +1,17 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """Tests for config env-var parsing.""" from __future__ import annotations diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_processor.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_processor.py index 4e534e98e..88bea3842 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_processor.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_processor.py @@ -1,3 +1,17 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """Unit tests for MAFSemanticProcessor span enrichment. Tests verify that the processor correctly: diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_react_step.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_react_step.py index bb1b7804e..dcd4b153f 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_react_step.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_react_step.py @@ -1,3 +1,17 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """Tests for react_step_patch module. These tests do not require ``agent-framework-core`` to be installed. They From 603c6c73cf2a26294f95f0f70282679c5390d191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=81=E5=B1=BF?= Date: Mon, 29 Jun 2026 11:28:56 +0800 Subject: [PATCH 10/21] fix(maf): finalize spans through util genai bridge --- .../README.rst | 24 +- .../microsoft_agent_framework/__init__.py | 22 +- .../util_genai_bridge.py | 637 ++++++++++++++++++ .../tests/test_util_genai_bridge.py | 299 ++++++++ 4 files changed, 967 insertions(+), 15 deletions(-) create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/util_genai_bridge.py create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_util_genai_bridge.py diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/README.rst b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/README.rst index 8145ff529..843fce423 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/README.rst +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/README.rst @@ -6,16 +6,20 @@ This package provides LoongSuite instrumentation for `Microsoft Agent Framework `_ (``agent-framework-core``). -It implements the hybrid "SpanProcessor + optional ReAct step patch" plan -described in ``llm-dev/microsoft-agent-framework/investigate/execute.md``: - -* ``MAFSemanticProcessor`` enriches the native OTel spans emitted by MAF's - ``ChatTelemetryLayer`` / ``EmbeddingTelemetryLayer`` / - ``AgentTelemetryLayer`` / workflow helpers with the ARMS GenAI semantic - conventions (``gen_ai.span.kind``, ``gen_ai.operation.name``, normalized - attribute names, ``gen_ai.response.time_to_first_token``, etc.) and - aggregates the 6 ARMS gauges. Successful spans keep the OpenTelemetry - default ``UNSET`` status; failed spans keep MAF's ``ERROR`` status. +It keeps MAF's native OTel span lifetime, but routes GenAI span finalization +through ``opentelemetry-util-genai`` before spans are ended: + +* ``util_genai_bridge`` patches MAF's native span helper functions and calls + the shared ``opentelemetry-util-genai`` invocation finish helpers for + ``AGENT`` / ``LLM`` / ``TOOL`` / ``EMBEDDING`` spans while the span is still + recording. This ensures exporter snapshots include + ``gen_ai.span.kind``, ``gen_ai.operation.name``, normalized provider names, + token usage, finish reasons, and streaming TTFT where MAF exposes enough + data. +* ``MAFSemanticProcessor`` remains as a compatibility layer for MAF workflow, + MCP, private-prefix attribute normalization, and the in-process ARMS gauges. + Successful spans keep the OpenTelemetry default ``UNSET`` status; failed + spans keep MAF's ``ERROR`` status. * ``react_step_patch`` (opt-in via ``ARMS_MAF_REACT_STEP_ENABLED=true``) wraps ``FunctionInvocationLayer.get_response`` so that each LLM round-trip inside the ReAct loop emits one ``react step`` span via diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/__init__.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/__init__.py index 7c682e3e3..15930c667 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/__init__.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/__init__.py @@ -15,9 +15,10 @@ """OpenTelemetry instrumentation for Microsoft Agent Framework. This instrumentor enables MAF's built-in OTel telemetry (``enable_instrumentation`` -with ``force=True`` so a sticky user-disable does not block us) and registers a -:class:`~.span_processor.MAFSemanticProcessor` that enriches MAF's native -spans to align with the ARMS GenAI semantic conventions. +with ``force=True`` so a sticky user-disable does not block us), bridges MAF's +native span helpers through ``opentelemetry-util-genai`` finish helpers, and +registers :class:`~.span_processor.MAFSemanticProcessor` for workflow/MCP +normalization plus metrics aggregation. The optional ReAct step patch (``ARMS_MAF_REACT_STEP_ENABLED=true``) wraps the ``FunctionInvocationLayer.get_response`` ReAct loop with @@ -50,6 +51,10 @@ from .package import _instruments from .react_step_patch import apply_react_step_patch, revert_react_step_patch from .span_processor import MAFSemanticProcessor +from .util_genai_bridge import ( + apply_util_genai_bridge, + revert_util_genai_bridge, +) from .version import __version__ __all__ = ["MicrosoftAgentFrameworkInstrumentor", "__version__"] @@ -107,7 +112,13 @@ def _instrument(self, **kwargs: Any) -> None: exc, ) - # 2) Register the semantic SpanProcessor. MAF uses the standard OTel + # 2) Bridge MAF's native span helper functions through util-genai's + # invocation finish helpers. This keeps MAF's span lifetime and + # streaming cleanup behavior, but writes AGENT/LLM/TOOL semantic + # attributes before span.end() creates the exporter snapshot. + apply_util_genai_bridge() + + # 3) Register the semantic SpanProcessor. MAF uses the standard OTel # TracerProvider (it does not have its own multi-processor), so # ``add_span_processor`` is the right hook. processor = MAFSemanticProcessor( @@ -163,7 +174,7 @@ def _instrument(self, **kwargs: Any) -> None: self._processor = processor self._tracer_provider = tracer_provider - # 3) Optional ReAct step patch (default OFF). + # 4) Optional ReAct step patch (default OFF). react_enabled = bool( kwargs.get( "react_step_enabled", is_react_step_enabled(default=False) @@ -180,6 +191,7 @@ def _uninstrument(self, **kwargs: Any) -> None: if self._react_applied: revert_react_step_patch() self._react_applied = False + revert_util_genai_bridge() if self._processor is not None: try: if self._tracer_provider is not None: diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/util_genai_bridge.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/util_genai_bridge.py new file mode 100644 index 000000000..16b38e361 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/util_genai_bridge.py @@ -0,0 +1,637 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Bridge MAF native spans through ``opentelemetry-util-genai`` semantics. + +Microsoft Agent Framework already owns correct span lifetime and streaming +cleanup behavior. This bridge keeps those native spans, but patches MAF's span +creation helpers so util-genai's invocation finish helpers run while the span is +still recording. That keeps LoongSuite GenAI attributes in the SDK snapshot +seen by exporters instead of relying on post-end SpanProcessor mutation. +""" + +from __future__ import annotations + +import contextlib +import json +import logging +import timeit +from typing import Any, Callable, Generator, Mapping, Optional + +from opentelemetry.trace import Span as OtelSpan +from opentelemetry.trace import SpanKind +from opentelemetry.util.genai.extended_span_utils import ( + _apply_embedding_finish_attributes, + _apply_execute_tool_finish_attributes, + _apply_invoke_agent_finish_attributes, +) +from opentelemetry.util.genai.extended_types import ( + EmbeddingInvocation, + ExecuteToolInvocation, + InvokeAgentInvocation, +) +from opentelemetry.util.genai.span_utils import _apply_llm_finish_attributes +from opentelemetry.util.genai.types import LLMInvocation + +from .semantic_conventions import ( + GEN_AI_OPERATION_NAME, + GEN_AI_PROVIDER_NAME, + GEN_AI_REQUEST_MODEL, + GEN_AI_RESPONSE_FINISH_REASONS, + GEN_AI_RESPONSE_MODEL, + GEN_AI_RESPONSE_TTFT, + GEN_AI_SPAN_KIND, + GEN_AI_USAGE_INPUT_TOKENS, + GEN_AI_USAGE_OUTPUT_TOKENS, + GenAIOperation, + GenAISpanKind, +) +from .span_processor import ( + _attr_value, + _classify_span, + _is_maf_span, + _normalize_provider, + _set_span_kind, + _ttft_from_events, +) + +logger = logging.getLogger(__name__) + +_applied = False +_original_get_span: Any = None +_original_start_streaming_span: Any = None +_original_activate_span: Any = None +_original_get_function_span: Any = None +_original_create_mcp_client_span: Any = None +_original_tools_get_function_span: Any = None +_original_mcp_create_mcp_client_span: Any = None + +_FINALIZED_ATTR = "_loongsuite_util_genai_finalized" +_END_WRAPPED_ATTR = "_loongsuite_util_genai_end_wrapped" +_STREAM_START_ATTR = "_loongsuite_util_genai_stream_start_s" +_STREAM_FIRST_TOKEN_ATTR = "_loongsuite_util_genai_stream_first_token_s" + + +def apply_util_genai_bridge() -> None: + """Patch MAF span helpers so GenAI spans are finalized by util-genai.""" + global _applied + global _original_create_mcp_client_span + global _original_activate_span + global _original_mcp_create_mcp_client_span + global _original_get_function_span + global _original_get_span + global _original_start_streaming_span + global _original_tools_get_function_span + + if _applied: + return + + try: + import agent_framework.observability as observability # type: ignore + except ImportError as exc: + logger.warning("MAF util-genai bridge skipped: %s", exc) + return + + _original_get_span = getattr(observability, "_get_span", None) + _original_start_streaming_span = getattr( + observability, "_start_streaming_span", None + ) + _original_activate_span = getattr(observability, "_activate_span", None) + _original_get_function_span = getattr( + observability, "get_function_span", None + ) + _original_create_mcp_client_span = getattr( + observability, "create_mcp_client_span", None + ) + + wrapped_get_span = ( + _wrap_get_span(_original_get_span) + if _original_get_span is not None + else None + ) + wrapped_start_streaming_span = ( + _wrap_start_streaming_span(_original_start_streaming_span) + if _original_start_streaming_span is not None + else None + ) + wrapped_get_function_span = ( + _wrap_get_function_span(_original_get_function_span) + if _original_get_function_span is not None + else None + ) + wrapped_create_mcp_client_span = ( + _wrap_create_mcp_client_span(_original_create_mcp_client_span) + if _original_create_mcp_client_span is not None + else None + ) + + if wrapped_get_span is not None: + observability._get_span = wrapped_get_span # type: ignore[attr-defined] + if _original_start_streaming_span is not None: + observability._start_streaming_span = wrapped_start_streaming_span # type: ignore[attr-defined] + if _original_activate_span is not None: + observability._activate_span = _wrap_activate_span( # type: ignore[attr-defined] + _original_activate_span + ) + if wrapped_get_function_span is not None: + observability.get_function_span = wrapped_get_function_span # type: ignore[attr-defined] + if wrapped_create_mcp_client_span is not None: + observability.create_mcp_client_span = wrapped_create_mcp_client_span # type: ignore[attr-defined] + + try: + import agent_framework._tools as tools_mod # type: ignore + + _original_tools_get_function_span = getattr( + tools_mod, "get_function_span", None + ) + if wrapped_get_function_span is not None: + tools_mod.get_function_span = wrapped_get_function_span # type: ignore[attr-defined] + except ImportError: + _original_tools_get_function_span = None + + try: + import agent_framework._mcp as mcp_mod # type: ignore + + _original_mcp_create_mcp_client_span = getattr( + mcp_mod, "create_mcp_client_span", None + ) + if wrapped_create_mcp_client_span is not None: + mcp_mod.create_mcp_client_span = wrapped_create_mcp_client_span # type: ignore[attr-defined] + except ImportError: + _original_mcp_create_mcp_client_span = None + + _applied = True + logger.info("MAF util-genai span bridge applied.") + + +def revert_util_genai_bridge() -> None: + """Restore MAF span helpers patched by :func:`apply_util_genai_bridge`.""" + global _applied + global _original_create_mcp_client_span + global _original_activate_span + global _original_mcp_create_mcp_client_span + global _original_get_function_span + global _original_get_span + global _original_start_streaming_span + global _original_tools_get_function_span + + if not _applied: + return + try: + import agent_framework.observability as observability # type: ignore + + if _original_get_span is not None: + observability._get_span = _original_get_span # type: ignore[attr-defined] + if _original_start_streaming_span is not None: + observability._start_streaming_span = _original_start_streaming_span # type: ignore[attr-defined] + if _original_activate_span is not None: + observability._activate_span = _original_activate_span # type: ignore[attr-defined] + if _original_get_function_span is not None: + observability.get_function_span = _original_get_function_span # type: ignore[attr-defined] + if _original_create_mcp_client_span is not None: + observability.create_mcp_client_span = _original_create_mcp_client_span # type: ignore[attr-defined] + except ImportError: + pass + try: + import agent_framework._tools as tools_mod # type: ignore + + if _original_tools_get_function_span is not None: + tools_mod.get_function_span = _original_tools_get_function_span # type: ignore[attr-defined] + except ImportError: + pass + try: + import agent_framework._mcp as mcp_mod # type: ignore + + if _original_mcp_create_mcp_client_span is not None: + mcp_mod.create_mcp_client_span = _original_mcp_create_mcp_client_span # type: ignore[attr-defined] + except ImportError: + pass + + _applied = False + _original_get_span = None + _original_start_streaming_span = None + _original_activate_span = None + _original_get_function_span = None + _original_create_mcp_client_span = None + _original_tools_get_function_span = None + _original_mcp_create_mcp_client_span = None + + +def _wrap_get_span(original: Callable[..., Any]) -> Callable[..., Any]: + @contextlib.contextmanager + def _get_span( + attributes: dict[str, Any], + span_name_attribute: str, + ) -> Generator[OtelSpan, Any, Any]: + _prepare_start_attributes(attributes) + with original(attributes, span_name_attribute) as span: + try: + yield span + finally: + _finalize_with_util_genai(span) + + return _get_span + + +def _wrap_start_streaming_span(original: Callable[..., Any]) -> Callable[..., Any]: + def _start_streaming_span( + attributes: dict[str, Any], span_name_attribute: str + ) -> OtelSpan: + _prepare_start_attributes(attributes) + span = original(attributes, span_name_attribute) + _mark_stream_start(span) + _wrap_span_end(span) + return span + + return _start_streaming_span + + +def _wrap_activate_span(original: Callable[..., Any]) -> Callable[..., Any]: + @contextlib.contextmanager + def _activate_span(span: OtelSpan) -> Generator[None, Any, Any]: + with original(span): + try: + yield + except Exception: + raise + else: + _record_first_stream_pull(span) + + return _activate_span + + +def _wrap_get_function_span(original: Callable[..., Any]) -> Callable[..., Any]: + @contextlib.contextmanager + def _get_function_span( + attributes: dict[str, Any], + ) -> Generator[OtelSpan, Any, Any]: + _prepare_start_attributes(attributes) + with original(attributes) as span: + try: + yield span + finally: + _finalize_with_util_genai(span) + + return _get_function_span + + +def _wrap_create_mcp_client_span(original: Callable[..., Any]) -> Callable[..., Any]: + @contextlib.contextmanager + def _create_mcp_client_span( + method_name: str, + target: str | None = None, + attributes: dict[str, Any] | None = None, + ) -> Generator[OtelSpan, Any, Any]: + bridge_attrs = dict(attributes or {}) + bridge_attrs[GEN_AI_OPERATION_NAME] = GenAIOperation.MCP + bridge_attrs[GEN_AI_SPAN_KIND] = GenAISpanKind.CLIENT + with original(method_name, target, bridge_attrs) as span: + yield span + + return _create_mcp_client_span + + +def _wrap_span_end(span: OtelSpan) -> None: + if getattr(span, _END_WRAPPED_ATTR, False): + return + original_end = getattr(span, "end", None) + if original_end is None: + return + + def _end(*args: Any, **kwargs: Any) -> Any: + _finalize_with_util_genai(span) + return original_end(*args, **kwargs) + + try: + setattr(span, "end", _end) + setattr(span, _END_WRAPPED_ATTR, True) + except Exception as exc: # pragma: no cover - SDK defensive + logger.warning( + "MAF streaming span finalization bridge disabled: %s", exc + ) + + +def _mark_stream_start(span: OtelSpan) -> None: + try: + setattr(span, _STREAM_START_ATTR, timeit.default_timer()) + except Exception: + pass + + +def _record_first_stream_pull(span: OtelSpan) -> None: + # MAF registers ``_activate_span(span)`` through + # ``ResponseStream.with_pull_context_manager``. That factory is entered and + # exited once per ``__anext__`` pull, so the first successful exit marks the + # first streamed update rather than final stream cleanup. + if getattr(span, _STREAM_FIRST_TOKEN_ATTR, None) is not None: + return + try: + start_s = getattr(span, _STREAM_START_ATTR, None) + first_s = timeit.default_timer() + setattr(span, _STREAM_FIRST_TOKEN_ATTR, first_s) + if start_s is None: + return + delta_ns = int(max(first_s - float(start_s), 0.0) * 1_000_000_000) + if delta_ns > 0 and not _attr_value(span, GEN_AI_RESPONSE_TTFT): + span.set_attribute(GEN_AI_RESPONSE_TTFT, delta_ns) + except Exception as exc: # pragma: no cover - defensive + logger.debug("could not record MAF streaming TTFT: %s", exc) + + +def _prepare_start_attributes(attributes: dict[str, Any]) -> None: + """Seed attributes known before MAF creates the span.""" + op_name = _mapping_value(attributes, GEN_AI_OPERATION_NAME) + span_name = _span_name_from_attributes(attributes) + span_kind, classified_op = _classify_span( + span_name, op_name if isinstance(op_name, str) else None, attributes + ) + if not _is_maf_span( + span_name, op_name if isinstance(op_name, str) else None, attributes + ): + return + if not _mapping_value(attributes, GEN_AI_OPERATION_NAME): + attributes[GEN_AI_OPERATION_NAME] = classified_op + elif classified_op == GenAIOperation.MCP: + attributes[GEN_AI_OPERATION_NAME] = classified_op + if not _mapping_value(attributes, GEN_AI_SPAN_KIND): + attributes[GEN_AI_SPAN_KIND] = span_kind + provider = _normalize_provider(_mapping_value(attributes, GEN_AI_PROVIDER_NAME)) + if provider is not None: + attributes[GEN_AI_PROVIDER_NAME] = provider + + +def _finalize_with_util_genai(span: OtelSpan) -> None: + if getattr(span, _FINALIZED_ATTR, False): + return + try: + name = getattr(span, "name", "") or "" + existing_op = _attr_value(span, GEN_AI_OPERATION_NAME) + existing_op = existing_op if isinstance(existing_op, str) else None + if not _is_maf_span(name, existing_op, span): + return + + span_kind, op_name = _classify_span(name, existing_op, span) + _set_common_live_attributes(span, span_kind, op_name) + + if span_kind == GenAISpanKind.LLM: + _apply_llm_finish_attributes(span, _llm_invocation(span, op_name)) + _set_span_kind(span, span, SpanKind.CLIENT) + ttft = _ttft_from_live_span(span) + if ttft is not None and not _attr_value(span, GEN_AI_RESPONSE_TTFT): + span.set_attribute(GEN_AI_RESPONSE_TTFT, ttft) + elif span_kind == GenAISpanKind.AGENT: + _apply_invoke_agent_finish_attributes( + span, _invoke_agent_invocation(span) + ) + elif span_kind == GenAISpanKind.TOOL: + _apply_execute_tool_finish_attributes( + span, _execute_tool_invocation(span) + ) + elif span_kind == GenAISpanKind.EMBEDDING: + _apply_embedding_finish_attributes( + span, _embedding_invocation(span) + ) + except Exception as exc: # pragma: no cover - defensive + logger.warning("MAF util-genai bridge finalize failed: %s", exc) + finally: + try: + setattr(span, _FINALIZED_ATTR, True) + except Exception: + pass + + +def _set_common_live_attributes( + span: OtelSpan, span_kind: str, op_name: str +) -> None: + if not _attr_value(span, GEN_AI_SPAN_KIND): + span.set_attribute(GEN_AI_SPAN_KIND, span_kind) + current_op = _attr_value(span, GEN_AI_OPERATION_NAME) + if not current_op or ( + current_op != op_name + and span_kind + in {GenAISpanKind.AGENT, GenAISpanKind.TASK, GenAISpanKind.CLIENT} + ): + span.set_attribute(GEN_AI_OPERATION_NAME, op_name) + provider = _normalize_provider(_attr_value(span, GEN_AI_PROVIDER_NAME)) + if provider is not None: + span.set_attribute(GEN_AI_PROVIDER_NAME, provider) + finish_reasons = _finish_reasons(span) + if finish_reasons: + span.set_attribute(GEN_AI_RESPONSE_FINISH_REASONS, finish_reasons) + + +def _llm_invocation(span: OtelSpan, op_name: str) -> LLMInvocation: + return LLMInvocation( + request_model=_string_attr(span, GEN_AI_REQUEST_MODEL) + or _model_from_span_name(span, "chat ") + or "unknown", + operation_name=op_name or GenAIOperation.CHAT, + provider=_string_attr(span, GEN_AI_PROVIDER_NAME), + response_model_name=_string_attr(span, GEN_AI_RESPONSE_MODEL), + finish_reasons=_finish_reasons(span), + input_tokens=_int_attr(span, GEN_AI_USAGE_INPUT_TOKENS), + output_tokens=_int_attr(span, GEN_AI_USAGE_OUTPUT_TOKENS), + temperature=_float_attr(span, "gen_ai.request.temperature"), + top_p=_float_attr(span, "gen_ai.request.top_p"), + frequency_penalty=_float_attr( + span, "gen_ai.request.frequency_penalty" + ), + presence_penalty=_float_attr(span, "gen_ai.request.presence_penalty"), + max_tokens=_int_attr(span, "gen_ai.request.max_tokens"), + stop_sequences=_string_list_attr(span, "gen_ai.request.stop_sequences"), + seed=_int_attr(span, "gen_ai.request.seed"), + conversation_id=_string_attr(span, "gen_ai.conversation.id"), + choice_count=_int_attr(span, "gen_ai.request.choice.count"), + ) + + +def _invoke_agent_invocation(span: OtelSpan) -> InvokeAgentInvocation: + return InvokeAgentInvocation( + provider=_string_attr(span, GEN_AI_PROVIDER_NAME) or "", + agent_name=_string_attr(span, "gen_ai.agent.name") + or _model_from_span_name(span, "invoke_agent "), + agent_id=_string_attr(span, "gen_ai.agent.id"), + agent_description=_string_attr(span, "gen_ai.agent.description"), + conversation_id=_string_attr(span, "gen_ai.conversation.id"), + request_model=_string_attr(span, GEN_AI_REQUEST_MODEL), + response_model_name=_string_attr(span, GEN_AI_RESPONSE_MODEL), + finish_reasons=_finish_reasons(span), + input_tokens=_int_attr(span, GEN_AI_USAGE_INPUT_TOKENS), + output_tokens=_int_attr(span, GEN_AI_USAGE_OUTPUT_TOKENS), + temperature=_float_attr(span, "gen_ai.request.temperature"), + top_p=_float_attr(span, "gen_ai.request.top_p"), + frequency_penalty=_float_attr( + span, "gen_ai.request.frequency_penalty" + ), + presence_penalty=_float_attr(span, "gen_ai.request.presence_penalty"), + max_tokens=_int_attr(span, "gen_ai.request.max_tokens"), + stop_sequences=_string_list_attr(span, "gen_ai.request.stop_sequences"), + seed=_int_attr(span, "gen_ai.request.seed"), + choice_count=_int_attr(span, "gen_ai.request.choice.count"), + ) + + +def _execute_tool_invocation(span: OtelSpan) -> ExecuteToolInvocation: + return ExecuteToolInvocation( + tool_name=_string_attr(span, "gen_ai.tool.name") + or _model_from_span_name(span, "execute_tool ") + or "unknown", + provider=_string_attr(span, GEN_AI_PROVIDER_NAME), + tool_call_id=_string_attr(span, "gen_ai.tool.call.id"), + tool_description=_string_attr(span, "gen_ai.tool.description"), + tool_type=_string_attr(span, "gen_ai.tool.type"), + ) + + +def _embedding_invocation(span: OtelSpan) -> EmbeddingInvocation: + return EmbeddingInvocation( + request_model=_string_attr(span, GEN_AI_REQUEST_MODEL) + or _model_from_span_name(span, "embeddings ") + or "unknown", + provider=_string_attr(span, GEN_AI_PROVIDER_NAME), + response_model_name=_string_attr(span, GEN_AI_RESPONSE_MODEL), + input_tokens=_int_attr(span, GEN_AI_USAGE_INPUT_TOKENS), + ) + + +def _mapping_value(attributes: Mapping[Any, Any], key: str) -> Any: + if key in attributes: + return attributes[key] + for attr_key, value in attributes.items(): + if str(attr_key) == key: + return value + return None + + +def _span_name_from_attributes(attributes: Mapping[Any, Any]) -> str: + op = _mapping_value(attributes, GEN_AI_OPERATION_NAME) + if op == GenAIOperation.CHAT: + model = _mapping_value(attributes, GEN_AI_REQUEST_MODEL) or "unknown" + return f"chat {model}" + if op == GenAIOperation.EMBEDDINGS: + model = _mapping_value(attributes, GEN_AI_REQUEST_MODEL) or "unknown" + return f"embeddings {model}" + if op == GenAIOperation.INVOKE_AGENT: + name = _mapping_value(attributes, "gen_ai.agent.name") or "unknown" + return f"invoke_agent {name}" + if op == GenAIOperation.EXECUTE_TOOL: + name = _mapping_value(attributes, "gen_ai.tool.name") or "unknown" + return f"execute_tool {name}" + method = _mapping_value(attributes, "mcp.method.name") + if method: + return str(method) + return str(op or "") + + +def _string_attr(span: Any, key: str) -> Optional[str]: + value = _attr_value(span, key) + if value is None: + return None + if isinstance(value, (list, tuple)): + if not value: + return None + value = value[0] + return value if isinstance(value, str) else str(value) + + +def _int_attr(span: Any, key: str) -> Optional[int]: + value = _attr_value(span, key) + if isinstance(value, bool) or value is None: + return None + try: + return int(value) + except (TypeError, ValueError): + return None + + +def _float_attr(span: Any, key: str) -> Optional[float]: + value = _attr_value(span, key) + if isinstance(value, bool) or value is None: + return None + try: + return float(value) + except (TypeError, ValueError): + return None + + +def _string_list_attr(span: Any, key: str) -> Optional[list[str]]: + value = _attr_value(span, key) + if value is None: + return None + if isinstance(value, str): + return [value] + if isinstance(value, (list, tuple)) and all( + isinstance(item, str) for item in value + ): + return list(value) + return None + + +def _finish_reasons(span: Any) -> Optional[list[str]]: + value = _attr_value(span, GEN_AI_RESPONSE_FINISH_REASONS) + if value is None: + return None + if isinstance(value, str): + try: + parsed = json.loads(value) + except (TypeError, ValueError): + return [value] + if isinstance(parsed, list) and all( + isinstance(item, str) for item in parsed + ): + return parsed + return [value] + if isinstance(value, (list, tuple)) and all( + isinstance(item, str) for item in value + ): + return list(value) + return None + + +def _model_from_span_name(span: Any, prefix: str) -> Optional[str]: + name = getattr(span, "name", "") or "" + if not isinstance(name, str) or not name.startswith(prefix): + return None + value = name[len(prefix) :].strip() + return value or None + + +def _ttft_from_live_span(span: OtelSpan) -> Optional[int]: + ttft = _ttft_from_events(span) + if ttft is not None: + return ttft + start_s = getattr(span, _STREAM_START_ATTR, None) + first_s = getattr(span, _STREAM_FIRST_TOKEN_ATTR, None) + if start_s is not None and first_s is not None: + try: + return int(max(float(first_s) - float(start_s), 0.0) * 1_000_000_000) + except (TypeError, ValueError): + return None + events = getattr(span, "_events", None) + if events is None: + return None + + class _ReadableLike: + pass + + readable = _ReadableLike() + readable.events = events + readable.start_time = getattr(span, "start_time", None) + return _ttft_from_events(readable) + + +__all__ = [ + "apply_util_genai_bridge", + "revert_util_genai_bridge", +] diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_util_genai_bridge.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_util_genai_bridge.py new file mode 100644 index 000000000..88b33e65b --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_util_genai_bridge.py @@ -0,0 +1,299 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for the MAF util-genai bridge. + +These tests use a tiny fake ``agent_framework.observability`` module so they do +not depend on the real MAF package. The important contract is exporter-visible: +attributes must be written before ``span.end()`` snapshots the span. +""" + +from __future__ import annotations + +import contextlib +import sys +import types + +from opentelemetry import trace +from opentelemetry.instrumentation.microsoft_agent_framework import ( + util_genai_bridge, +) +from opentelemetry.instrumentation.microsoft_agent_framework.semantic_conventions import ( + GEN_AI_OPERATION_NAME, + GEN_AI_PROVIDER_NAME, + GEN_AI_REQUEST_MODEL, + GEN_AI_RESPONSE_FINISH_REASONS, + GEN_AI_RESPONSE_TTFT, + GEN_AI_SPAN_KIND, + GEN_AI_USAGE_INPUT_TOKENS, + GEN_AI_USAGE_OUTPUT_TOKENS, + GenAIOperation, + GenAISpanKind, +) +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) + + +def _install_fake_observability(monkeypatch): + tp = TracerProvider() + exporter = InMemorySpanExporter() + tp.add_span_processor(SimpleSpanProcessor(exporter)) + tracer = tp.get_tracer("fake-maf") + + @contextlib.contextmanager + def _get_span(attributes, span_name_attribute): + operation = attributes.get(GEN_AI_OPERATION_NAME, "operation") + span_name = attributes.get(span_name_attribute, "unknown") + span = tracer.start_span(f"{operation} {span_name}") + span.set_attributes(attributes) + with trace.use_span( + span, + end_on_exit=True, + record_exception=False, + set_status_on_exception=False, + ) as current_span: + yield current_span + + def _start_streaming_span(attributes, span_name_attribute): + operation = attributes.get(GEN_AI_OPERATION_NAME, "operation") + span_name = attributes.get(span_name_attribute, "unknown") + span = tracer.start_span(f"{operation} {span_name}") + span.set_attributes(attributes) + return span + + @contextlib.contextmanager + def _activate_span(span): + with trace.use_span(span, end_on_exit=False): + yield + + @contextlib.contextmanager + def get_function_span(attributes): + span = tracer.start_span( + f"{attributes[GEN_AI_OPERATION_NAME]} {attributes['gen_ai.tool.name']}" + ) + span.set_attributes(attributes) + with trace.use_span( + span, + end_on_exit=True, + record_exception=False, + set_status_on_exception=False, + ) as current_span: + yield current_span + + @contextlib.contextmanager + def create_mcp_client_span(method_name, target=None, attributes=None): + span_name = f"{method_name} {target}" if target else method_name + span = tracer.start_span(span_name, kind=trace.SpanKind.CLIENT) + span.set_attribute("mcp.method.name", method_name) + if attributes: + span.set_attributes(attributes) + with trace.use_span(span, end_on_exit=True) as current_span: + yield current_span + + obs_mod = types.ModuleType("agent_framework.observability") + obs_mod._get_span = _get_span + obs_mod._start_streaming_span = _start_streaming_span + obs_mod._activate_span = _activate_span + obs_mod.get_function_span = get_function_span + obs_mod.create_mcp_client_span = create_mcp_client_span + + af_mod = types.ModuleType("agent_framework") + af_mod.observability = obs_mod + tools_mod = types.ModuleType("agent_framework._tools") + tools_mod.get_function_span = get_function_span + mcp_mod = types.ModuleType("agent_framework._mcp") + mcp_mod.create_mcp_client_span = create_mcp_client_span + monkeypatch.setitem(sys.modules, "agent_framework", af_mod) + monkeypatch.setitem(sys.modules, "agent_framework.observability", obs_mod) + monkeypatch.setitem(sys.modules, "agent_framework._tools", tools_mod) + monkeypatch.setitem(sys.modules, "agent_framework._mcp", mcp_mod) + util_genai_bridge.revert_util_genai_bridge() + return obs_mod, exporter + + +def test_llm_get_span_is_finalized_by_util_genai_before_export(monkeypatch): + obs_mod, exporter = _install_fake_observability(monkeypatch) + util_genai_bridge.apply_util_genai_bridge() + try: + attributes = { + GEN_AI_OPERATION_NAME: GenAIOperation.CHAT, + GEN_AI_PROVIDER_NAME: "azure_openai", + GEN_AI_REQUEST_MODEL: "qwen-plus", + GEN_AI_USAGE_INPUT_TOKENS: 11, + GEN_AI_USAGE_OUTPUT_TOKENS: 13, + GEN_AI_RESPONSE_FINISH_REASONS: '["stop"]', + } + with obs_mod._get_span(attributes, GEN_AI_REQUEST_MODEL): + pass + finally: + util_genai_bridge.revert_util_genai_bridge() + + span = exporter.get_finished_spans()[0] + assert span.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.LLM + assert span.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.CHAT + assert span.attributes.get(GEN_AI_PROVIDER_NAME) == "openai" + assert span.attributes.get(GEN_AI_USAGE_INPUT_TOKENS) == 11 + assert span.attributes.get(GEN_AI_USAGE_OUTPUT_TOKENS) == 13 + assert span.attributes.get(GEN_AI_RESPONSE_FINISH_REASONS) == ("stop",) + assert span.kind == trace.SpanKind.CLIENT + + +def test_streaming_llm_end_wrapper_finalizes_before_export(monkeypatch): + obs_mod, exporter = _install_fake_observability(monkeypatch) + util_genai_bridge.apply_util_genai_bridge() + try: + span = obs_mod._start_streaming_span( + { + GEN_AI_OPERATION_NAME: GenAIOperation.CHAT, + GEN_AI_REQUEST_MODEL: "qwen-plus", + }, + GEN_AI_REQUEST_MODEL, + ) + with obs_mod._activate_span(span): + pass + span.end() + finally: + util_genai_bridge.revert_util_genai_bridge() + + exported = exporter.get_finished_spans()[0] + assert exported.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.LLM + assert exported.attributes.get(GEN_AI_RESPONSE_TTFT) is not None + + +def test_embedding_span_is_finalized_by_util_genai_before_export(monkeypatch): + obs_mod, exporter = _install_fake_observability(monkeypatch) + util_genai_bridge.apply_util_genai_bridge() + try: + with obs_mod._get_span( + { + GEN_AI_OPERATION_NAME: GenAIOperation.EMBEDDINGS, + GEN_AI_PROVIDER_NAME: "openai", + GEN_AI_REQUEST_MODEL: "text-embedding-v4", + GEN_AI_USAGE_INPUT_TOKENS: 17, + }, + GEN_AI_REQUEST_MODEL, + ): + pass + finally: + util_genai_bridge.revert_util_genai_bridge() + + span = exporter.get_finished_spans()[0] + assert span.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.EMBEDDING + assert span.attributes.get(GEN_AI_OPERATION_NAME) == ( + GenAIOperation.EMBEDDINGS + ) + assert span.attributes.get(GEN_AI_REQUEST_MODEL) == "text-embedding-v4" + assert span.attributes.get(GEN_AI_USAGE_INPUT_TOKENS) == 17 + + +def test_tool_span_is_finalized_by_util_genai_before_export(monkeypatch): + obs_mod, exporter = _install_fake_observability(monkeypatch) + util_genai_bridge.apply_util_genai_bridge() + try: + tools_mod = sys.modules["agent_framework._tools"] + with tools_mod.get_function_span( + { + GEN_AI_OPERATION_NAME: GenAIOperation.EXECUTE_TOOL, + "gen_ai.tool.name": "city_score", + "gen_ai.tool.call.id": "call-1", + "gen_ai.tool.type": "function", + } + ): + pass + finally: + util_genai_bridge.revert_util_genai_bridge() + + span = exporter.get_finished_spans()[0] + assert span.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.TOOL + assert span.attributes.get(GEN_AI_OPERATION_NAME) == ( + GenAIOperation.EXECUTE_TOOL + ) + assert span.attributes.get("gen_ai.tool.name") == "city_score" + assert span.attributes.get("gen_ai.tool.call.id") == "call-1" + + +def test_agent_span_is_finalized_by_util_genai_before_export(monkeypatch): + obs_mod, exporter = _install_fake_observability(monkeypatch) + util_genai_bridge.apply_util_genai_bridge() + try: + with obs_mod._get_span( + { + GEN_AI_OPERATION_NAME: GenAIOperation.INVOKE_AGENT, + GEN_AI_PROVIDER_NAME: "microsoft.agent_framework", + "gen_ai.agent.name": "planner", + "gen_ai.agent.id": "agent-1", + }, + "gen_ai.agent.name", + ): + pass + finally: + util_genai_bridge.revert_util_genai_bridge() + + span = exporter.get_finished_spans()[0] + assert span.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.AGENT + assert span.attributes.get(GEN_AI_OPERATION_NAME) == ( + GenAIOperation.INVOKE_AGENT + ) + assert span.attributes.get("gen_ai.agent.name") == "planner" + assert span.attributes.get("gen_ai.agent.id") == "agent-1" + + +def test_mcp_span_is_seeded_before_export(monkeypatch): + obs_mod, exporter = _install_fake_observability(monkeypatch) + util_genai_bridge.apply_util_genai_bridge() + try: + mcp_mod = sys.modules["agent_framework._mcp"] + with mcp_mod.create_mcp_client_span("tools/call", "city_score"): + pass + finally: + util_genai_bridge.revert_util_genai_bridge() + + span = exporter.get_finished_spans()[0] + assert span.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.CLIENT + assert span.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.MCP + + +def test_apply_revert_apply_keeps_single_wrapper_layer(monkeypatch): + obs_mod, _ = _install_fake_observability(monkeypatch) + original_get_span = obs_mod._get_span + original_start_streaming_span = obs_mod._start_streaming_span + tools_mod = sys.modules["agent_framework._tools"] + original_tool_span = tools_mod.get_function_span + + util_genai_bridge.apply_util_genai_bridge() + first_get_span = obs_mod._get_span + first_streaming = obs_mod._start_streaming_span + first_tool_span = tools_mod.get_function_span + util_genai_bridge.revert_util_genai_bridge() + assert obs_mod._get_span is original_get_span + assert obs_mod._start_streaming_span is original_start_streaming_span + assert tools_mod.get_function_span is original_tool_span + + util_genai_bridge.apply_util_genai_bridge() + try: + assert obs_mod._get_span is not original_get_span + assert obs_mod._start_streaming_span is not original_start_streaming_span + assert tools_mod.get_function_span is not original_tool_span + assert obs_mod._get_span is not first_get_span + assert obs_mod._start_streaming_span is not first_streaming + assert tools_mod.get_function_span is not first_tool_span + finally: + util_genai_bridge.revert_util_genai_bridge() + + assert obs_mod._get_span is original_get_span + assert obs_mod._start_streaming_span is original_start_streaming_span + assert tools_mod.get_function_span is original_tool_span From 7e262e4907989a3c4aad0d10c35120e7fbb8c9c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=81=E5=B1=BF?= Date: Mon, 29 Jun 2026 15:43:19 +0800 Subject: [PATCH 11/21] fix(maf): address util genai review comments --- .../span_processor.py | 23 +- .../util_genai_bridge.py | 299 ++++++++++++++---- .../tests/test_processor.py | 10 + .../tests/test_util_genai_bridge.py | 98 +++++- 4 files changed, 365 insertions(+), 65 deletions(-) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py index 07eef3cfc..4bcb6e30e 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py @@ -403,8 +403,8 @@ def _ttft_from_events(readable: Any) -> Optional[int]: """Backfill ``gen_ai.response.time_to_first_token`` (ns) from the first streaming chunk event timestamp. - MAF emits streaming chunks as span events; the first event's timestamp - minus the span start time is the TTFT. + MAF emits streaming chunks as span events; the first non-exception event's + timestamp minus the span start time is the TTFT. """ events = getattr(readable, "events", None) or () if not events: @@ -412,6 +412,8 @@ def _ttft_from_events(readable: Any) -> Optional[int]: start_time = getattr(readable, "start_time", None) first_ts = None for ev in events: + if _is_exception_event(ev): + continue ts = getattr(ev, "timestamp", None) if ts is None: ts = ev.get("timestamp") if isinstance(ev, dict) else None @@ -426,6 +428,23 @@ def _ttft_from_events(readable: Any) -> Optional[int]: return None +def _is_exception_event(event: Any) -> bool: + name = getattr(event, "name", None) + if name is None and isinstance(event, dict): + name = event.get("name") + if name == "exception": + return True + attributes = getattr(event, "attributes", None) + if attributes is None and isinstance(event, dict): + attributes = event.get("attributes") + return bool( + isinstance(attributes, dict) + and ( + "exception.type" in attributes or "exception.message" in attributes + ) + ) + + # ---------- Metrics aggregation ---------- diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/util_genai_bridge.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/util_genai_bridge.py index 16b38e361..3c5b2e70e 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/util_genai_bridge.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/util_genai_bridge.py @@ -24,25 +24,43 @@ from __future__ import annotations import contextlib +import inspect import json import logging import timeit -from typing import Any, Callable, Generator, Mapping, Optional +from typing import Any, AsyncGenerator, Callable, Generator, Mapping, Optional +from opentelemetry import trace as otel_trace from opentelemetry.trace import Span as OtelSpan from opentelemetry.trace import SpanKind -from opentelemetry.util.genai.extended_span_utils import ( - _apply_embedding_finish_attributes, - _apply_execute_tool_finish_attributes, - _apply_invoke_agent_finish_attributes, -) -from opentelemetry.util.genai.extended_types import ( - EmbeddingInvocation, - ExecuteToolInvocation, - InvokeAgentInvocation, -) -from opentelemetry.util.genai.span_utils import _apply_llm_finish_attributes -from opentelemetry.util.genai.types import LLMInvocation + +try: + from opentelemetry.util.genai.extended_span_utils import ( + _apply_embedding_finish_attributes, + _apply_execute_tool_finish_attributes, + _apply_invoke_agent_finish_attributes, + ) + from opentelemetry.util.genai.extended_types import ( + EmbeddingInvocation, + ExecuteToolInvocation, + InvokeAgentInvocation, + ) + from opentelemetry.util.genai.span_utils import ( + _apply_llm_finish_attributes, + ) + from opentelemetry.util.genai.types import LLMInvocation +except ImportError as exc: + _UTIL_GENAI_IMPORT_ERROR: Optional[ImportError] = exc + _apply_embedding_finish_attributes = None + _apply_execute_tool_finish_attributes = None + _apply_invoke_agent_finish_attributes = None + _apply_llm_finish_attributes = None + EmbeddingInvocation = None + ExecuteToolInvocation = None + InvokeAgentInvocation = None + LLMInvocation = None +else: + _UTIL_GENAI_IMPORT_ERROR = None from .semantic_conventions import ( GEN_AI_OPERATION_NAME, @@ -60,9 +78,9 @@ from .span_processor import ( _attr_value, _classify_span, + _is_exception_event, _is_maf_span, _normalize_provider, - _set_span_kind, _ttft_from_events, ) @@ -96,6 +114,13 @@ def apply_util_genai_bridge() -> None: if _applied: return + if _UTIL_GENAI_IMPORT_ERROR is not None: + logger.warning( + "MAF util-genai bridge skipped: opentelemetry-util-genai " + "finish helpers unavailable: %s", + _UTIL_GENAI_IMPORT_ERROR, + ) + return try: import agent_framework.observability as observability # type: ignore @@ -194,13 +219,17 @@ def revert_util_genai_bridge() -> None: if _original_get_span is not None: observability._get_span = _original_get_span # type: ignore[attr-defined] if _original_start_streaming_span is not None: - observability._start_streaming_span = _original_start_streaming_span # type: ignore[attr-defined] + observability._start_streaming_span = ( + _original_start_streaming_span # type: ignore[attr-defined] + ) if _original_activate_span is not None: observability._activate_span = _original_activate_span # type: ignore[attr-defined] if _original_get_function_span is not None: observability.get_function_span = _original_get_function_span # type: ignore[attr-defined] if _original_create_mcp_client_span is not None: - observability.create_mcp_client_span = _original_create_mcp_client_span # type: ignore[attr-defined] + observability.create_mcp_client_span = ( + _original_create_mcp_client_span # type: ignore[attr-defined] + ) except ImportError: pass try: @@ -214,7 +243,9 @@ def revert_util_genai_bridge() -> None: import agent_framework._mcp as mcp_mod # type: ignore if _original_mcp_create_mcp_client_span is not None: - mcp_mod.create_mcp_client_span = _original_mcp_create_mcp_client_span # type: ignore[attr-defined] + mcp_mod.create_mcp_client_span = ( + _original_mcp_create_mcp_client_span # type: ignore[attr-defined] + ) except ImportError: pass @@ -234,8 +265,11 @@ def _get_span( attributes: dict[str, Any], span_name_attribute: str, ) -> Generator[OtelSpan, Any, Any]: - _prepare_start_attributes(attributes) - with original(attributes, span_name_attribute) as span: + bridge_attrs = _prepare_start_attributes(attributes) + span_cm = _current_span_context( + original, bridge_attrs, span_name_attribute + ) + with span_cm as span: try: yield span finally: @@ -244,12 +278,16 @@ def _get_span( return _get_span -def _wrap_start_streaming_span(original: Callable[..., Any]) -> Callable[..., Any]: +def _wrap_start_streaming_span( + original: Callable[..., Any], +) -> Callable[..., Any]: def _start_streaming_span( attributes: dict[str, Any], span_name_attribute: str ) -> OtelSpan: - _prepare_start_attributes(attributes) - span = original(attributes, span_name_attribute) + bridge_attrs = _prepare_start_attributes(attributes) + span = _start_streaming_span_with_kind( + original, bridge_attrs, span_name_attribute + ) _mark_stream_start(span) _wrap_span_end(span) return span @@ -258,26 +296,83 @@ def _start_streaming_span( def _wrap_activate_span(original: Callable[..., Any]) -> Callable[..., Any]: - @contextlib.contextmanager - def _activate_span(span: OtelSpan) -> Generator[None, Any, Any]: - with original(span): - try: - yield - except Exception: - raise - else: - _record_first_stream_pull(span) + if inspect.isasyncgenfunction(original): + + @contextlib.asynccontextmanager + async def _async_activate_span( + span: OtelSpan, + ) -> AsyncGenerator[None, Any]: + async with original(span): + try: + yield + except Exception: + raise + else: + _record_first_stream_pull(span) + + return _async_activate_span + + if inspect.iscoroutinefunction(original): + + @contextlib.asynccontextmanager + async def _awaited_activate_span( + span: OtelSpan, + ) -> AsyncGenerator[None, Any]: + cm = await original(span) + async with cm: + try: + yield + except Exception: + raise + else: + _record_first_stream_pull(span) + + return _awaited_activate_span + + def _activate_span(span: OtelSpan) -> Any: + cm = original(span) + if hasattr(cm, "__aenter__"): + return _async_activate_context(span, cm) + return _sync_activate_context(span, cm) return _activate_span -def _wrap_get_function_span(original: Callable[..., Any]) -> Callable[..., Any]: +@contextlib.contextmanager +def _sync_activate_context( + span: OtelSpan, cm: Any +) -> Generator[None, Any, Any]: + with cm: + try: + yield + except Exception: + raise + else: + _record_first_stream_pull(span) + + +@contextlib.asynccontextmanager +async def _async_activate_context( + span: OtelSpan, cm: Any +) -> AsyncGenerator[None, Any]: + async with cm: + try: + yield + except Exception: + raise + else: + _record_first_stream_pull(span) + + +def _wrap_get_function_span( + original: Callable[..., Any], +) -> Callable[..., Any]: @contextlib.contextmanager def _get_function_span( attributes: dict[str, Any], ) -> Generator[OtelSpan, Any, Any]: - _prepare_start_attributes(attributes) - with original(attributes) as span: + bridge_attrs = _prepare_start_attributes(attributes) + with original(bridge_attrs) as span: try: yield span finally: @@ -286,7 +381,9 @@ def _get_function_span( return _get_function_span -def _wrap_create_mcp_client_span(original: Callable[..., Any]) -> Callable[..., Any]: +def _wrap_create_mcp_client_span( + original: Callable[..., Any], +) -> Callable[..., Any]: @contextlib.contextmanager def _create_mcp_client_span( method_name: str, @@ -302,6 +399,71 @@ def _create_mcp_client_span( return _create_mcp_client_span +def _current_span_context( + original: Callable[..., Any], + attributes: dict[str, Any], + span_name_attribute: str, +) -> Any: + kind = _otel_start_kind(attributes) + if kind is None: + return original(attributes, span_name_attribute) + return _start_current_span_with_kind(attributes, span_name_attribute, kind) + + +def _start_streaming_span_with_kind( + original: Callable[..., Any], + attributes: dict[str, Any], + span_name_attribute: str, +) -> OtelSpan: + kind = _otel_start_kind(attributes) + if kind is None: + return original(attributes, span_name_attribute) + return _start_detached_span_with_kind( + attributes, span_name_attribute, kind + ) + + +def _otel_start_kind(attributes: Mapping[Any, Any]) -> SpanKind | None: + span_kind = _mapping_value(attributes, GEN_AI_SPAN_KIND) + if span_kind in {GenAISpanKind.LLM, GenAISpanKind.EMBEDDING}: + return SpanKind.CLIENT + return None + + +def _start_current_span_with_kind( + attributes: dict[str, Any], + span_name_attribute: str, + kind: SpanKind, +) -> Any: + span = _start_detached_span_with_kind( + attributes, span_name_attribute, kind + ) + return otel_trace.use_span( + span=span, + end_on_exit=True, + record_exception=False, + set_status_on_exception=False, + ) + + +def _start_detached_span_with_kind( + attributes: dict[str, Any], + span_name_attribute: str, + kind: SpanKind, +) -> OtelSpan: + import agent_framework.observability as observability # type: ignore + + operation = ( + _mapping_value(attributes, GEN_AI_OPERATION_NAME) or "operation" + ) + span_name = _mapping_value(attributes, span_name_attribute) or "unknown" + span = observability.get_tracer().start_span( + f"{operation} {span_name}", kind=kind + ) + span.set_attributes(attributes) + return span + + def _wrap_span_end(span: OtelSpan) -> None: if getattr(span, _END_WRAPPED_ATTR, False): return @@ -333,42 +495,42 @@ def _record_first_stream_pull(span: OtelSpan) -> None: # MAF registers ``_activate_span(span)`` through # ``ResponseStream.with_pull_context_manager``. That factory is entered and # exited once per ``__anext__`` pull, so the first successful exit marks the - # first streamed update rather than final stream cleanup. + # first streamed update rather than final stream cleanup. The context + # manager API does not expose the update object, so keep this as an internal + # fallback marker and let finalization prefer any real TTFT event emitted by + # the framework/provider before writing the public GenAI attribute. if getattr(span, _STREAM_FIRST_TOKEN_ATTR, None) is not None: return try: - start_s = getattr(span, _STREAM_START_ATTR, None) - first_s = timeit.default_timer() - setattr(span, _STREAM_FIRST_TOKEN_ATTR, first_s) - if start_s is None: - return - delta_ns = int(max(first_s - float(start_s), 0.0) * 1_000_000_000) - if delta_ns > 0 and not _attr_value(span, GEN_AI_RESPONSE_TTFT): - span.set_attribute(GEN_AI_RESPONSE_TTFT, delta_ns) + setattr(span, _STREAM_FIRST_TOKEN_ATTR, timeit.default_timer()) except Exception as exc: # pragma: no cover - defensive logger.debug("could not record MAF streaming TTFT: %s", exc) -def _prepare_start_attributes(attributes: dict[str, Any]) -> None: +def _prepare_start_attributes(attributes: Mapping[Any, Any]) -> dict[str, Any]: """Seed attributes known before MAF creates the span.""" - op_name = _mapping_value(attributes, GEN_AI_OPERATION_NAME) - span_name = _span_name_from_attributes(attributes) + bridge_attrs = dict(attributes) + op_name = _mapping_value(bridge_attrs, GEN_AI_OPERATION_NAME) + span_name = _span_name_from_attributes(bridge_attrs) span_kind, classified_op = _classify_span( - span_name, op_name if isinstance(op_name, str) else None, attributes + span_name, op_name if isinstance(op_name, str) else None, bridge_attrs ) if not _is_maf_span( - span_name, op_name if isinstance(op_name, str) else None, attributes + span_name, op_name if isinstance(op_name, str) else None, bridge_attrs ): - return - if not _mapping_value(attributes, GEN_AI_OPERATION_NAME): - attributes[GEN_AI_OPERATION_NAME] = classified_op + return bridge_attrs + if not _mapping_value(bridge_attrs, GEN_AI_OPERATION_NAME): + bridge_attrs[GEN_AI_OPERATION_NAME] = classified_op elif classified_op == GenAIOperation.MCP: - attributes[GEN_AI_OPERATION_NAME] = classified_op - if not _mapping_value(attributes, GEN_AI_SPAN_KIND): - attributes[GEN_AI_SPAN_KIND] = span_kind - provider = _normalize_provider(_mapping_value(attributes, GEN_AI_PROVIDER_NAME)) + bridge_attrs[GEN_AI_OPERATION_NAME] = classified_op + if not _mapping_value(bridge_attrs, GEN_AI_SPAN_KIND): + bridge_attrs[GEN_AI_SPAN_KIND] = span_kind + provider = _normalize_provider( + _mapping_value(bridge_attrs, GEN_AI_PROVIDER_NAME) + ) if provider is not None: - attributes[GEN_AI_PROVIDER_NAME] = provider + bridge_attrs[GEN_AI_PROVIDER_NAME] = provider + return bridge_attrs def _finalize_with_util_genai(span: OtelSpan) -> None: @@ -386,9 +548,10 @@ def _finalize_with_util_genai(span: OtelSpan) -> None: if span_kind == GenAISpanKind.LLM: _apply_llm_finish_attributes(span, _llm_invocation(span, op_name)) - _set_span_kind(span, span, SpanKind.CLIENT) ttft = _ttft_from_live_span(span) - if ttft is not None and not _attr_value(span, GEN_AI_RESPONSE_TTFT): + if ttft is not None and not _attr_value( + span, GEN_AI_RESPONSE_TTFT + ): span.set_attribute(GEN_AI_RESPONSE_TTFT, ttft) elif span_kind == GenAISpanKind.AGENT: _apply_invoke_agent_finish_attributes( @@ -449,7 +612,9 @@ def _llm_invocation(span: OtelSpan, op_name: str) -> LLMInvocation: ), presence_penalty=_float_attr(span, "gen_ai.request.presence_penalty"), max_tokens=_int_attr(span, "gen_ai.request.max_tokens"), - stop_sequences=_string_list_attr(span, "gen_ai.request.stop_sequences"), + stop_sequences=_string_list_attr( + span, "gen_ai.request.stop_sequences" + ), seed=_int_attr(span, "gen_ai.request.seed"), conversation_id=_string_attr(span, "gen_ai.conversation.id"), choice_count=_int_attr(span, "gen_ai.request.choice.count"), @@ -476,7 +641,9 @@ def _invoke_agent_invocation(span: OtelSpan) -> InvokeAgentInvocation: ), presence_penalty=_float_attr(span, "gen_ai.request.presence_penalty"), max_tokens=_int_attr(span, "gen_ai.request.max_tokens"), - stop_sequences=_string_list_attr(span, "gen_ai.request.stop_sequences"), + stop_sequences=_string_list_attr( + span, "gen_ai.request.stop_sequences" + ), seed=_int_attr(span, "gen_ai.request.seed"), choice_count=_int_attr(span, "gen_ai.request.choice.count"), ) @@ -611,11 +778,19 @@ def _ttft_from_live_span(span: OtelSpan) -> Optional[int]: ttft = _ttft_from_events(span) if ttft is not None: return ttft + status = getattr(span, "status", None) + if getattr(status, "status_code", None) == otel_trace.StatusCode.ERROR: + return None + events = getattr(span, "events", None) or getattr(span, "_events", None) + if events is not None and any(_is_exception_event(ev) for ev in events): + return None start_s = getattr(span, _STREAM_START_ATTR, None) first_s = getattr(span, _STREAM_FIRST_TOKEN_ATTR, None) if start_s is not None and first_s is not None: try: - return int(max(float(first_s) - float(start_s), 0.0) * 1_000_000_000) + return int( + max(float(first_s) - float(start_s), 0.0) * 1_000_000_000 + ) except (TypeError, ValueError): return None events = getattr(span, "_events", None) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_processor.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_processor.py index 88bea3842..d6da4d632 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_processor.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_processor.py @@ -209,6 +209,16 @@ def test_ttft_backfill_from_first_event(): assert ttft is not None and ttft > 0 +def test_ttft_backfill_skips_exception_event(): + tp, tracer, exporter, _ = _setup() + with tracer.start_as_current_span("chat gpt-4o") as span: + span.set_attribute(GEN_AI_OPERATION_NAME, GenAIOperation.CHAT) + span.set_attribute(GEN_AI_PROVIDER_NAME, "openai") + span.add_event("exception", {"exception.type": "RuntimeError"}) + spans = _flush(exporter) + assert GEN_AI_RESPONSE_TTFT not in spans[0].attributes + + def test_finish_reasons_json_string_normalized_to_array(): tp, tracer, exporter, _ = _setup() with tracer.start_as_current_span("chat gpt-4o") as span: diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_util_genai_bridge.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_util_genai_bridge.py index 88b33e65b..54d69566f 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_util_genai_bridge.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_util_genai_bridge.py @@ -21,6 +21,7 @@ from __future__ import annotations +import asyncio import contextlib import sys import types @@ -110,6 +111,7 @@ def create_mcp_client_span(method_name, target=None, attributes=None): obs_mod._activate_span = _activate_span obs_mod.get_function_span = get_function_span obs_mod.create_mcp_client_span = create_mcp_client_span + obs_mod.get_tracer = lambda: tracer af_mod = types.ModuleType("agent_framework") af_mod.observability = obs_mod @@ -142,6 +144,8 @@ def test_llm_get_span_is_finalized_by_util_genai_before_export(monkeypatch): finally: util_genai_bridge.revert_util_genai_bridge() + assert GEN_AI_SPAN_KIND not in attributes + assert attributes[GEN_AI_PROVIDER_NAME] == "azure_openai" span = exporter.get_finished_spans()[0] assert span.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.LLM assert span.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.CHAT @@ -172,6 +176,53 @@ def test_streaming_llm_end_wrapper_finalizes_before_export(monkeypatch): exported = exporter.get_finished_spans()[0] assert exported.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.LLM assert exported.attributes.get(GEN_AI_RESPONSE_TTFT) is not None + assert exported.kind == trace.SpanKind.CLIENT + + +def test_streaming_error_does_not_emit_fallback_ttft(monkeypatch): + obs_mod, exporter = _install_fake_observability(monkeypatch) + util_genai_bridge.apply_util_genai_bridge() + try: + span = obs_mod._start_streaming_span( + { + GEN_AI_OPERATION_NAME: GenAIOperation.CHAT, + GEN_AI_REQUEST_MODEL: "qwen-not-a-real-model", + }, + GEN_AI_REQUEST_MODEL, + ) + with obs_mod._activate_span(span): + pass + span.set_status(trace.Status(trace.StatusCode.ERROR)) + span.end() + finally: + util_genai_bridge.revert_util_genai_bridge() + + exported = exporter.get_finished_spans()[0] + assert exported.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.LLM + assert GEN_AI_RESPONSE_TTFT not in exported.attributes + + +def test_streaming_exception_event_does_not_emit_fallback_ttft(monkeypatch): + obs_mod, exporter = _install_fake_observability(monkeypatch) + util_genai_bridge.apply_util_genai_bridge() + try: + span = obs_mod._start_streaming_span( + { + GEN_AI_OPERATION_NAME: GenAIOperation.CHAT, + GEN_AI_REQUEST_MODEL: "qwen-not-a-real-model", + }, + GEN_AI_REQUEST_MODEL, + ) + with obs_mod._activate_span(span): + pass + span.add_event("exception", {"exception.type": "RuntimeError"}) + span.end() + finally: + util_genai_bridge.revert_util_genai_bridge() + + exported = exporter.get_finished_spans()[0] + assert exported.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.LLM + assert GEN_AI_RESPONSE_TTFT not in exported.attributes def test_embedding_span_is_finalized_by_util_genai_before_export(monkeypatch): @@ -286,7 +337,9 @@ def test_apply_revert_apply_keeps_single_wrapper_layer(monkeypatch): util_genai_bridge.apply_util_genai_bridge() try: assert obs_mod._get_span is not original_get_span - assert obs_mod._start_streaming_span is not original_start_streaming_span + assert ( + obs_mod._start_streaming_span is not original_start_streaming_span + ) assert tools_mod.get_function_span is not original_tool_span assert obs_mod._get_span is not first_get_span assert obs_mod._start_streaming_span is not first_streaming @@ -297,3 +350,46 @@ def test_apply_revert_apply_keeps_single_wrapper_layer(monkeypatch): assert obs_mod._get_span is original_get_span assert obs_mod._start_streaming_span is original_start_streaming_span assert tools_mod.get_function_span is original_tool_span + + +def test_apply_skips_when_util_genai_private_helpers_are_unavailable( + monkeypatch, +): + obs_mod, _ = _install_fake_observability(monkeypatch) + original_get_span = obs_mod._get_span + + monkeypatch.setattr( + util_genai_bridge, + "_UTIL_GENAI_IMPORT_ERROR", + ImportError("missing private helper"), + ) + + util_genai_bridge.apply_util_genai_bridge() + + assert obs_mod._get_span is original_get_span + + +def test_activate_span_wrapper_supports_async_context_manager(): + events = [] + + @contextlib.asynccontextmanager + async def _activate_span(span): + events.append("enter") + try: + yield + finally: + events.append("exit") + + wrapped = util_genai_bridge._wrap_activate_span(_activate_span) + span = types.SimpleNamespace() + + async def _run(): + async with wrapped(span): + events.append("body") + + asyncio.run(_run()) + + assert events == ["enter", "body", "exit"] + assert ( + getattr(span, util_genai_bridge._STREAM_FIRST_TOKEN_ATTR) is not None + ) From c9a3503c87a5530afc3788c76b4be27a6c52a8c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=81=E5=B1=BF?= Date: Mon, 29 Jun 2026 22:08:06 +0800 Subject: [PATCH 12/21] fix(maf): align workflow and mcp genai semantics --- .../semantic_conventions.py | 31 +-- .../span_processor.py | 182 ++++++++++++------ .../util_genai_bridge.py | 14 +- .../tests/test_processor.py | 130 ++++++++----- .../tests/test_util_genai_bridge.py | 3 +- 5 files changed, 232 insertions(+), 128 deletions(-) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py index 8dc7652d9..9a7cc88ff 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py @@ -34,13 +34,12 @@ class GenAISpanKind: LLM = "LLM" TOOL = "TOOL" EMBEDDING = "EMBEDDING" - CHAIN = "CHAIN" - TASK = "TASK" + WORKFLOW = "WORKFLOW" STEP = "STEP" ENTRY = "ENTRY" RETRIEVER = "RETRIEVER" RERANKER = "RERANKER" - CLIENT = "CLIENT" + MCP = "MCP" class GenAIOperation: @@ -54,10 +53,9 @@ class GenAIOperation: CREATE_AGENT = "create_agent" INVOKE_AGENT = "invoke_agent" RETRIEVAL = "retrieval" - WORKFLOW = "workflow" - TASK = "task" + WORKFLOW = "invoke_workflow" REACT = "react" - MCP = "mcp" + MCP = EXECUTE_TOOL # MAF span-name prefixes (from observability.py OtelAttr) — used to classify a @@ -89,25 +87,10 @@ class GenAIOperation: # if MAF later writes the canonical key directly, the rename becomes a no-op # because the source key won't be present. MAF_ATTR_RENAME_MAP: Final[dict[str, str]] = { - # Workflow / chain attributes (observability.py:247-281) - "workflow.id": "gen_ai.workflow.id", + # Workflow attributes. The LoongSuite registry currently defines only + # ``gen_ai.workflow.name``; keep other MAF-private keys under their original + # names instead of extending the ``gen_ai`` namespace with unregistered keys. "workflow.name": "gen_ai.workflow.name", - "workflow.description": "gen_ai.workflow.description", - "workflow.definition": "gen_ai.workflow.definition", - "workflow_builder.name": "gen_ai.workflow.builder.name", - "workflow_builder.description": "gen_ai.workflow.builder.description", - # Executor / task attributes (observability.py:266-272) - "executor.id": "gen_ai.task.name", - "executor.type": "gen_ai.task.type", - # Edge group attributes (observability.py:270-274) - "edge_group.type": "gen_ai.edge_group.type", - "edge_group.id": "gen_ai.edge_group.id", - # Message attributes (observability.py:276-281) - "message.source_id": "gen_ai.message.source_id", - "message.target_id": "gen_ai.message.target_id", - "message.type": "gen_ai.message.type", - "message.payload_type": "gen_ai.message.payload_type", - "message.destination_executor_id": "gen_ai.message.destination_executor_id", } # Provider name normalization — collapse known aliases to the canonical OTel/ARMS diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py index 4bcb6e30e..e1ca6b0c0 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py @@ -21,9 +21,11 @@ This processor: 1. Injects ``gen_ai.span.kind`` (MAF does not set it). -2. Renames MAF private-prefix attributes (``workflow.id`` → ``gen_ai.workflow.id``, - ``executor.id`` → ``gen_ai.task.name`` …) per the local mapping table in - :mod:`semantic_conventions`. +2. Copies registry-defined MAF private-prefix attributes + (``workflow.name`` → ``gen_ai.workflow.name``) per the local mapping table in + :mod:`semantic_conventions`; other MAF private attributes are kept under + their original names to avoid extending the ``gen_ai`` namespace with + unregistered keys. 3. Backfills ``gen_ai.response.time_to_first_token`` from the first streaming chunk event timestamp. 4. Normalizes ``gen_ai.provider.name`` (``azure_openai`` → ``openai``). @@ -91,6 +93,7 @@ _EXECUTOR_PROCESS = "executor.process" _EDGE_GROUP_PROCESS = "edge_group.process" _LIVE_SPAN_MAX_AGE_NS = 60 * 1_000_000_000 +_GEN_AI_TOOL_NAME = "gen_ai.tool.name" def _attr_value(span: Any, key: str) -> Any: @@ -230,6 +233,18 @@ def _rename_maf_attrs(live_span: OtelSpan, readable: Any) -> list[str]: return renamed +def _copy_maf_attrs(live_span: OtelSpan, readable: Any) -> list[str]: + """Copy MAF-private attributes to canonical keys without removing sources.""" + copied: list[str] = [] + for old_key, new_key in MAF_ATTR_RENAME_MAP.items(): + value = _attr_value(readable, old_key) + if value is None: + continue + _set_attr(live_span, new_key, value) + copied.append(new_key) + return copied + + def _normalize_provider(value: Any) -> Optional[str]: """Normalize ``gen_ai.provider.name`` to the ARMS canonical value. @@ -308,6 +323,17 @@ def _is_mcp_span(readable: Any) -> bool: return False +def _mcp_tool_name(name: str, readable: Any) -> Optional[str]: + """Return a low-cardinality tool name for an MCP client span.""" + method = _attr_value(readable, _MCP_METHOD_NAME_ATTR) + method = method if isinstance(method, str) else None + if method and name.startswith(f"{method} "): + target = name[len(method) + 1 :].strip() + if target: + return target + return method or (name.strip() if name else None) + + def _is_maf_span(name: str, operation: Optional[str], readable: Any) -> bool: """Return True when the span carries a Microsoft Agent Framework signal.""" if operation: @@ -344,7 +370,7 @@ def _classify_span( # write ``gen_ai.operation.name`` for MCP spans. We check the attribute # directly (cheap; happens once per span on_end). if _is_mcp_span(readable): - return GenAISpanKind.CLIENT, GenAIOperation.MCP + return GenAISpanKind.MCP, GenAIOperation.MCP if operation: op = operation @@ -380,23 +406,20 @@ def _classify_span( return GenAISpanKind.STEP, op if op == GenAIOperation.RETRIEVAL: return GenAISpanKind.RETRIEVER, op - if op == GenAIOperation.WORKFLOW or op == GenAIOperation.TASK: - # ``executor.process`` splits by ``executor.type``: - # FunctionExecutor -> TASK - # AgentExecutor -> AGENT (kind) + invoke_agent (op) - # anything else -> CHAIN + if op == GenAIOperation.WORKFLOW: + # ``executor.process`` splits by ``executor.type`` when it represents + # an agent invocation. Other executor/message/edge spans remain part of + # the workflow operation. if name.startswith(_EXECUTOR_PROCESS): executor_type = _attr_value(readable, "executor.type") if isinstance(executor_type, str): et = executor_type.lower() - if "function" in et: - return GenAISpanKind.TASK, GenAIOperation.TASK if "agent" in et: return GenAISpanKind.AGENT, GenAIOperation.INVOKE_AGENT - return GenAISpanKind.CHAIN, op + return GenAISpanKind.WORKFLOW, op if op == GenAIOperation.MCP: - return GenAISpanKind.CLIENT, op - return GenAISpanKind.CHAIN, op + return GenAISpanKind.MCP, op + return GenAISpanKind.WORKFLOW, op def _ttft_from_events(readable: Any) -> Optional[int]: @@ -701,6 +724,7 @@ def on_start( with self._live_span_lock: self._live_spans[key] = span self._span_parents[key] = parent_id + self._apply_semantic_attributes(span, span, remove_private_attrs=True) def on_end(self, span: Any) -> None: """Enrich a just-ended MAF span with ARMS GenAI semantic conventions.""" @@ -720,50 +744,15 @@ def on_end(self, span: Any) -> None: # ``_set_attr``). Same approach as the OpenInference processor. try: - name = span.name or "" - existing_op = _attr_value(span, GEN_AI_OPERATION_NAME) - existing_op = existing_op if isinstance(existing_op, str) else None - if not _is_maf_span(name, existing_op, span): + classified = self._apply_semantic_attributes( + live, span, remove_private_attrs=True + ) + if classified is None: return - span_kind, op_name = _classify_span(name, existing_op, span) - - # 1) gen_ai.span.kind (only set if not already present) - if not _attr_value(span, GEN_AI_SPAN_KIND): - _set_attr(live, GEN_AI_SPAN_KIND, span_kind) - - # 2) gen_ai.operation.name (set if missing or freshly derived for - # workflow spans where MAF does not write it). For spans MAF - # mislabels (e.g. MCP ``tools/call`` written by MAF as - # ``execute_tool`` — see ``create_mcp_client_span`` at - # ``observability.py:2101``) we also override when our - # classification disagrees, provided the span is one of the - # kinds whose operation.name we own (TASK/AGENT reclassification - # of ``executor.process``, plus CLIENT for MCP — MAF writes the - # LLM's ``execute_tool`` value onto MCP inner spans). - if not existing_op: - _set_attr(live, GEN_AI_OPERATION_NAME, op_name) - elif existing_op != op_name and span_kind in { - GenAISpanKind.TASK, - GenAISpanKind.AGENT, - GenAISpanKind.CLIENT, - }: - _set_attr(live, GEN_AI_OPERATION_NAME, op_name) - - # 3) Rename MAF private-prefix attributes - _rename_maf_attrs(live, span) - - # 4) Normalize provider.name - provider = _attr_value(span, GEN_AI_PROVIDER_NAME) - normalized = _normalize_provider(provider) - if normalized is not None and normalized != provider: - _set_attr(live, GEN_AI_PROVIDER_NAME, normalized) - - # 5) Normalize finish reasons written by MAF as a JSON string. - _normalize_finish_reasons(live, span) - - # 6) TTFT backfill for LLM spans with streaming events + span_kind, op_name = classified + + # TTFT backfill for LLM spans with streaming events if span_kind == GenAISpanKind.LLM: - _set_span_kind(live, span, SpanKind.CLIENT) ttft = _ttft_from_events(span) if ttft is not None and not _attr_value( span, GEN_AI_RESPONSE_TTFT @@ -801,6 +790,87 @@ def on_end(self, span: Any) -> None: # Span has already been ended by the SDK; nothing to do here. pass + def _apply_semantic_attributes( + self, + live: OtelSpan, + readable: Any, + *, + remove_private_attrs: bool, + ) -> Optional[Tuple[str, str]]: + """Write GenAI semantic attributes while the span is still exportable. + + ``on_start`` runs before any downstream exporter receives a completed + span, so workflow spans must be normalized there. ``on_end`` keeps the + same logic as a fallback for attributes written after span creation. + """ + name = getattr(readable, "name", "") or getattr(live, "name", "") or "" + existing_op = _attr_value(readable, GEN_AI_OPERATION_NAME) + existing_op = existing_op if isinstance(existing_op, str) else None + if not _is_maf_span(name, existing_op, readable): + return None + + span_kind, op_name = _classify_span(name, existing_op, readable) + existing_kind = _attr_value(readable, GEN_AI_SPAN_KIND) + if ( + isinstance(existing_kind, str) + and existing_kind + and span_kind == GenAISpanKind.WORKFLOW + and existing_kind != GenAISpanKind.WORKFLOW + ): + span_kind = existing_kind + + # 1) gen_ai.span.kind (only set if not already present) + if not existing_kind or ( + existing_kind != span_kind + and existing_kind == GenAISpanKind.WORKFLOW + and span_kind != GenAISpanKind.WORKFLOW + ): + _set_attr(live, GEN_AI_SPAN_KIND, span_kind) + + # 2) gen_ai.operation.name (set if missing or freshly derived for + # workflow spans where MAF does not write it). For spans MAF + # mislabels (e.g. MCP ``tools/call`` written by MAF as + # ``execute_tool`` — see ``create_mcp_client_span`` at + # ``observability.py:2101``) we also override when our + # classification disagrees, provided the span is one of the + # kinds whose operation.name we own (AGENT reclassification of + # ``executor.process``, plus MCP logical spans). + if not existing_op: + _set_attr(live, GEN_AI_OPERATION_NAME, op_name) + elif existing_op != op_name and span_kind in { + GenAISpanKind.AGENT, + GenAISpanKind.MCP, + }: + _set_attr(live, GEN_AI_OPERATION_NAME, op_name) + + if span_kind == GenAISpanKind.MCP and not _attr_value( + readable, _GEN_AI_TOOL_NAME + ): + tool_name = _mcp_tool_name(name, readable) + if tool_name: + _set_attr(live, _GEN_AI_TOOL_NAME, tool_name) + + # 3) Rename MAF private-prefix attributes + if remove_private_attrs: + _rename_maf_attrs(live, readable) + else: + _copy_maf_attrs(live, readable) + + # 4) Normalize provider.name + provider = _attr_value(readable, GEN_AI_PROVIDER_NAME) + normalized = _normalize_provider(provider) + if normalized is not None and normalized != provider: + _set_attr(live, GEN_AI_PROVIDER_NAME, normalized) + + # 5) Normalize finish reasons written by MAF as a JSON string. + _normalize_finish_reasons(live, readable) + + # 6) LLM/embedding spans should use CLIENT OTel span kind. + if span_kind == GenAISpanKind.LLM: + _set_span_kind(live, readable, SpanKind.CLIENT) + + return span_kind, op_name + def _aggregate_metrics( self, readable: Any, span_kind: str, op_name: str ) -> None: diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/util_genai_bridge.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/util_genai_bridge.py index 3c5b2e70e..fb58419b7 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/util_genai_bridge.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/util_genai_bridge.py @@ -80,6 +80,7 @@ _classify_span, _is_exception_event, _is_maf_span, + _mcp_tool_name, _normalize_provider, _ttft_from_events, ) @@ -392,7 +393,8 @@ def _create_mcp_client_span( ) -> Generator[OtelSpan, Any, Any]: bridge_attrs = dict(attributes or {}) bridge_attrs[GEN_AI_OPERATION_NAME] = GenAIOperation.MCP - bridge_attrs[GEN_AI_SPAN_KIND] = GenAISpanKind.CLIENT + bridge_attrs[GEN_AI_SPAN_KIND] = GenAISpanKind.MCP + bridge_attrs.setdefault("gen_ai.tool.name", target or method_name) with original(method_name, target, bridge_attrs) as span: yield span @@ -521,7 +523,7 @@ def _prepare_start_attributes(attributes: Mapping[Any, Any]) -> dict[str, Any]: return bridge_attrs if not _mapping_value(bridge_attrs, GEN_AI_OPERATION_NAME): bridge_attrs[GEN_AI_OPERATION_NAME] = classified_op - elif classified_op == GenAIOperation.MCP: + elif span_kind == GenAISpanKind.MCP: bridge_attrs[GEN_AI_OPERATION_NAME] = classified_op if not _mapping_value(bridge_attrs, GEN_AI_SPAN_KIND): bridge_attrs[GEN_AI_SPAN_KIND] = span_kind @@ -583,9 +585,15 @@ def _set_common_live_attributes( if not current_op or ( current_op != op_name and span_kind - in {GenAISpanKind.AGENT, GenAISpanKind.TASK, GenAISpanKind.CLIENT} + in {GenAISpanKind.AGENT, GenAISpanKind.MCP} ): span.set_attribute(GEN_AI_OPERATION_NAME, op_name) + if span_kind == GenAISpanKind.MCP and not _attr_value( + span, "gen_ai.tool.name" + ): + tool_name = _mcp_tool_name(getattr(span, "name", "") or "", span) + if tool_name: + span.set_attribute("gen_ai.tool.name", tool_name) provider = _normalize_provider(_attr_value(span, GEN_AI_PROVIDER_NAME)) if provider is not None: span.set_attribute(GEN_AI_PROVIDER_NAME, provider) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_processor.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_processor.py index d6da4d632..0ad1a5b87 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_processor.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_processor.py @@ -16,7 +16,7 @@ Tests verify that the processor correctly: - Injects ``gen_ai.span.kind`` for each MAF span type. -- Renames MAF private-prefix attributes (``workflow.id`` → ``gen_ai.workflow.id`` …). +- Copies registry-defined MAF attributes (``workflow.name`` → ``gen_ai.workflow.name``). - Reclassifies ``executor.process`` spans by ``executor.type``. - Normalizes ``gen_ai.provider.name``. - Backfills ``gen_ai.response.time_to_first_token`` from streaming events. @@ -65,6 +65,21 @@ def _setup(): return tp, tracer, exporter, processor +def _setup_exporter_before_processor(): + """Simulate exporters registered before the MAF semantic processor.""" + tp = TracerProvider() + exporter = InMemorySpanExporter() + processor = MAFSemanticProcessor( + meter_provider=None, + metrics_enabled=False, + capture_sensitive_data=False, + ) + tp.add_span_processor(SimpleSpanProcessor(exporter)) + tp.add_span_processor(processor) + tracer = tp.get_tracer("test") + return tp, tracer, exporter, processor + + def _flush(exporter): # Force spans to be exported to the in-memory exporter. return exporter.get_finished_spans() @@ -116,7 +131,7 @@ def test_agent_span(): assert spans[0].attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.AGENT -def test_workflow_run_span_gets_chain_kind_and_workflow_op(): +def test_workflow_run_span_gets_workflow_kind_and_invoke_workflow_op(): tp, tracer, exporter, _ = _setup() with tracer.start_as_current_span("workflow.run abc-123") as span: span.set_attribute("workflow.id", "abc-123") @@ -124,16 +139,33 @@ def test_workflow_run_span_gets_chain_kind_and_workflow_op(): span.set_attribute("workflow.description", "d") spans = _flush(exporter) s = spans[0] - assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.CHAIN + assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.WORKFLOW assert s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.WORKFLOW - # MAF private-prefix renamed to gen_ai.* canonical keys - assert s.attributes.get("gen_ai.workflow.id") == "abc-123" + # Only registry-defined workflow attributes are copied into gen_ai.*. assert s.attributes.get("gen_ai.workflow.name") == "MyWorkflow" - # The original MAF private key should be removed (best-effort). - assert "workflow.id" not in s.attributes + assert s.attributes.get("workflow.id") == "abc-123" + assert "gen_ai.workflow.id" not in s.attributes -def test_executor_process_function_executor_becomes_task(): +def test_workflow_start_attrs_are_exported_if_exporter_runs_first(): + tp, tracer, exporter, _ = _setup_exporter_before_processor() + with tracer.start_as_current_span( + "workflow.run abc-123", + attributes={ + "workflow.id": "abc-123", + "workflow.name": "MyWorkflow", + }, + ): + pass + s = _flush(exporter)[0] + assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.WORKFLOW + assert s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.WORKFLOW + assert s.attributes.get("gen_ai.workflow.name") == "MyWorkflow" + assert s.attributes.get("workflow.id") == "abc-123" + assert "gen_ai.workflow.id" not in s.attributes + + +def test_executor_process_function_executor_stays_workflow(): tp, tracer, exporter, _ = _setup() with tracer.start_as_current_span("executor.process fid-1") as span: # MAF does NOT set gen_ai.operation.name for executor.process spans. @@ -141,10 +173,27 @@ def test_executor_process_function_executor_becomes_task(): span.set_attribute("executor.type", "FunctionExecutor") spans = _flush(exporter) s = spans[0] - assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.TASK - assert s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.TASK - assert s.attributes.get("gen_ai.task.name") == "fid-1" - assert s.attributes.get("gen_ai.task.type") == "FunctionExecutor" + assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.WORKFLOW + assert s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.WORKFLOW + assert s.attributes.get("executor.id") == "fid-1" + assert "gen_ai.task.name" not in s.attributes + + +def test_executor_start_attrs_are_exported_if_exporter_runs_first(): + tp, tracer, exporter, _ = _setup_exporter_before_processor() + with tracer.start_as_current_span( + "executor.process fid-1", + attributes={ + "executor.id": "fid-1", + "executor.type": "FunctionExecutor", + }, + ): + pass + s = _flush(exporter)[0] + assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.WORKFLOW + assert s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.WORKFLOW + assert s.attributes.get("executor.id") == "fid-1" + assert "gen_ai.task.name" not in s.attributes def test_executor_process_agent_executor_becomes_agent(): @@ -160,14 +209,14 @@ def test_executor_process_agent_executor_becomes_agent(): ) -def test_executor_process_unknown_executor_stays_chain(): +def test_executor_process_unknown_executor_stays_workflow(): tp, tracer, exporter, _ = _setup() with tracer.start_as_current_span("executor.process xid") as span: span.set_attribute("executor.id", "xid") span.set_attribute("executor.type", "SomeOtherExecutor") spans = _flush(exporter) s = spans[0] - assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.CHAIN + assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.WORKFLOW assert s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.WORKFLOW @@ -178,9 +227,9 @@ def test_message_send_span(): span.set_attribute("message.target_id", "tgt") spans = _flush(exporter) s = spans[0] - assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.CHAIN - assert s.attributes.get("gen_ai.message.source_id") == "src" - assert s.attributes.get("gen_ai.message.target_id") == "tgt" + assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.WORKFLOW + assert s.attributes.get("message.source_id") == "src" + assert "gen_ai.message.source_id" not in s.attributes def test_provider_normalization_azure_openai_to_openai(): @@ -342,34 +391,24 @@ def test_non_maf_span_is_left_untouched(): assert not processor._counters.calls_count -def test_maf_dict_attribute_is_serialized_via_gen_ai_json_dumps(): - """Dict/list attribute values written into ``_attributes`` by MAF under - private prefixes must be JSON-serialized via - ``opentelemetry.util.genai.utils.gen_ai_json_dumps`` when renamed to - ``gen_ai.*`` keys, because OTel SDKs reject arbitrary dict attribute - values. Hard constraint #2. - - We exercise this directly through ``_set_attr`` because the SDK drops - dict values at ``set_attribute`` time (logged as a warning), which is - exactly the scenario our coercion defends against when MAF mutates - ``_attributes`` directly itself (it does so in several internal paths). - """ +def test_dict_attribute_is_serialized_via_gen_ai_json_dumps(): + """Dict/list GenAI values are JSON-serialized before SDK export.""" from opentelemetry.instrumentation.microsoft_agent_framework import ( span_processor as sp, ) tp, tracer, exporter, _ = _setup() - workflow_def = {"nodes": ["a", "b"], "edges": [{"from": "a", "to": "b"}]} + message = {"role": "user", "content": "hello"} # Drive the rename path through _set_attr (the same path on_end uses after # the SDK has stopped accepting set_attribute calls). with tracer.start_as_current_span("workflow.run xyz") as span: - sp._set_attr(span, "gen_ai.workflow.definition", workflow_def) + sp._set_attr(span, "gen_ai.input.messages", message) spans = _flush(exporter) s = spans[0] - val = s.attributes.get("gen_ai.workflow.definition") + val = s.attributes.get("gen_ai.input.messages") assert isinstance(val, str) - assert "nodes" in val and "edges" in val + assert "user" in val and "hello" in val def test_safe_dumps_uses_gen_ai_json_dumps(): @@ -396,11 +435,11 @@ def test_safe_dumps_truncates_at_4kb(): assert len(out) <= 4096 -def test_mcp_span_classified_as_client(): +def test_mcp_span_classified_as_mcp_execute_tool(): """MCP spans emitted by MAF's ``create_mcp_client_span`` carry no ``gen_ai.operation.name``; their name is ``{mcp.method.name} {target}`` (unbounded), so they must be detected via the ``mcp.method.name`` - attribute and classified as ``(CLIENT, mcp)``. Regression for [M1]. + attribute and classified as ``(MCP, execute_tool)``. Regression for [M1]. """ from opentelemetry.trace import SpanKind @@ -413,8 +452,9 @@ def test_mcp_span_classified_as_client(): span.set_attribute("mcp.session.id", "sess-1") spans = _flush(exporter) s = spans[0] - assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.CLIENT + assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.MCP assert s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.MCP + assert s.attributes.get("gen_ai.tool.name") == "get_weather" def test_mcp_span_via_client_kind_and_mcp_attr_fallback(): @@ -429,8 +469,9 @@ def test_mcp_span_via_client_kind_and_mcp_attr_fallback(): span.set_attribute("mcp.protocol.version", "2024-11-05") spans = _flush(exporter) s = spans[0] - assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.CLIENT + assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.MCP assert s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.MCP + assert s.attributes.get("gen_ai.tool.name") == "initialize" def test_non_mcp_client_span_is_not_misclassified_as_mcp(): @@ -449,16 +490,16 @@ def test_non_mcp_client_span_is_not_misclassified_as_mcp(): assert GEN_AI_OPERATION_NAME not in s.attributes -def test_mcp_span_op_name_overridden_to_mcp_when_maf_writes_execute_tool(): +def test_mcp_span_kind_set_when_maf_writes_execute_tool(): """[P1] regression: MAF emits ``gen_ai.operation.name=execute_tool`` on the MCP ``tools/call`` inner span (its ``create_mcp_client_span`` reuses the tool-call op name even though it sets ``mcp.method.name``). The processor - must override the op name to ``mcp`` so the span is not mislabeled as a - TOOL call in the ARMS pipeline. + must set the logical span kind to ``MCP`` so the span is not mislabeled as a + plain TOOL call in the ARMS pipeline. - Before the fix, ``on_end``'s op-name override only fired when - ``span_kind in {TASK, AGENT}`` — CLIENT (MCP) was missing, so the inner - span kept MAF's ``execute_tool`` value. + Before the fix, MCP spans were identified with a non-registry operation + value. The current LoongSuite profile uses ``MCP`` as span kind and + ``execute_tool`` as operation name. """ from opentelemetry.trace import SpanKind @@ -471,8 +512,9 @@ def test_mcp_span_op_name_overridden_to_mcp_when_maf_writes_execute_tool(): span.set_attribute(GEN_AI_OPERATION_NAME, GenAIOperation.EXECUTE_TOOL) spans = _flush(exporter) s = spans[0] - assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.CLIENT + assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.MCP assert s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.MCP + assert s.attributes.get("gen_ai.tool.name") == "slow_summary" def test_provider_normalization_keeps_framework_provider_separate(): diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_util_genai_bridge.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_util_genai_bridge.py index 54d69566f..38b7b26c7 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_util_genai_bridge.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_util_genai_bridge.py @@ -314,8 +314,9 @@ def test_mcp_span_is_seeded_before_export(monkeypatch): util_genai_bridge.revert_util_genai_bridge() span = exporter.get_finished_spans()[0] - assert span.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.CLIENT + assert span.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.MCP assert span.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.MCP + assert span.attributes.get("gen_ai.tool.name") == "city_score" def test_apply_revert_apply_keeps_single_wrapper_layer(monkeypatch): From 0b1fa9088c862a4b6c5adbf0ab4f657c251c4882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=81=E5=B1=BF?= Date: Mon, 29 Jun 2026 22:37:08 +0800 Subject: [PATCH 13/21] fix(maf): align workflow telemetry with genai util --- .../semantic_conventions.py | 1 - .../span_processor.py | 61 ++++++++++++------- .../util_genai_bridge.py | 29 +++++---- .../tests/test_processor.py | 32 +++++----- .../tests/test_util_genai_bridge.py | 19 +++++- .../gen_ai_extended_attributes.py | 6 ++ 6 files changed, 93 insertions(+), 55 deletions(-) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py index 9a7cc88ff..3ee12eda0 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/semantic_conventions.py @@ -55,7 +55,6 @@ class GenAIOperation: RETRIEVAL = "retrieval" WORKFLOW = "invoke_workflow" REACT = "react" - MCP = EXECUTE_TOOL # MAF span-name prefixes (from observability.py OtelAttr) — used to classify a diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py index e1ca6b0c0..b9a556d77 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py @@ -299,6 +299,7 @@ def _set_span_kind(live_span: OtelSpan, readable: Any, kind: SpanKind) -> None: _MCP_METHOD_NAME_ATTR = "mcp.method.name" +_MCP_TOOL_CALL_METHOD = "tools/call" def _is_mcp_span(readable: Any) -> bool: @@ -325,6 +326,11 @@ def _is_mcp_span(readable: Any) -> bool: def _mcp_tool_name(name: str, readable: Any) -> Optional[str]: """Return a low-cardinality tool name for an MCP client span.""" + tool_call_prefix = f"{_MCP_TOOL_CALL_METHOD} " + if name.startswith(tool_call_prefix): + target = name[len(tool_call_prefix) :].strip() + if target: + return target method = _attr_value(readable, _MCP_METHOD_NAME_ATTR) method = method if isinstance(method, str) else None if method and name.startswith(f"{method} "): @@ -334,11 +340,26 @@ def _mcp_tool_name(name: str, readable: Any) -> Optional[str]: return method or (name.strip() if name else None) +def _is_mcp_tool_call_span(name: str, readable: Any) -> bool: + """Return True only for MCP ``tools/call`` spans. + + MCP lifecycle operations such as ``initialize`` and ``tools/list`` are MCP + protocol spans, but they are not GenAI tool executions. Upstream MCP + semantic conventions only align MCP tool calls with GenAI ``execute_tool``. + """ + method = _attr_value(readable, _MCP_METHOD_NAME_ATTR) + if method == _MCP_TOOL_CALL_METHOD: + return True + return name.startswith(f"{_MCP_TOOL_CALL_METHOD} ") + + def _is_maf_span(name: str, operation: Optional[str], readable: Any) -> bool: """Return True when the span carries a Microsoft Agent Framework signal.""" + if _is_mcp_span(readable) and not _is_mcp_tool_call_span(name, readable): + return False if operation: return True - if _is_mcp_span(readable): + if _is_mcp_tool_call_span(name, readable): return True if name == _REACT_STEP_NAME: return True @@ -355,22 +376,19 @@ def _classify_span( Classification priority: 1. Existing ``gen_ai.operation.name`` (set by MAF for chat/embeddings/tool/agent). - 2. MCP attribute detection — MAF's ``create_mcp_client_span`` + 2. MCP tool-call detection — MAF's ``create_mcp_client_span`` (``observability.py:2083``) emits spans named ``{mcp.method.name} {target}`` - with no ``gen_ai.operation.name`` set; the ``mcp.method.name`` attribute - (always present) is the reliable signal. Falls back to - ``SpanKind.CLIENT`` + any ``mcp.*`` attribute. MCP is intentionally - *not* in ``MAF_SPAN_NAME_PREFIXES`` because its method names are - unbounded (``initialize``, ``tools/call`` …) and would collide with - other prefixes. + with no ``gen_ai.operation.name`` set. Only ``tools/call`` spans are + GenAI tool executions; lifecycle spans such as ``initialize`` keep only + their ``mcp.*`` protocol attributes. 3. Span-name prefix matching (workflow spans have no operation.name from MAF). 4. ``react step`` literal name (emitted by our react_step patch). """ - # MCP detection — runs before operation-based matching because MAF does not - # write ``gen_ai.operation.name`` for MCP spans. We check the attribute - # directly (cheap; happens once per span on_end). - if _is_mcp_span(readable): - return GenAISpanKind.MCP, GenAIOperation.MCP + # MCP tool calls are GenAI tool executions. Other MCP protocol lifecycle + # spans are filtered out by ``_is_maf_span`` and keep only their ``mcp.*`` + # attributes. + if _is_mcp_tool_call_span(name, readable): + return GenAISpanKind.MCP, GenAIOperation.EXECUTE_TOOL if operation: op = operation @@ -417,8 +435,6 @@ def _classify_span( if "agent" in et: return GenAISpanKind.AGENT, GenAIOperation.INVOKE_AGENT return GenAISpanKind.WORKFLOW, op - if op == GenAIOperation.MCP: - return GenAISpanKind.MCP, op return GenAISpanKind.WORKFLOW, op @@ -811,6 +827,7 @@ def _apply_semantic_attributes( span_kind, op_name = _classify_span(name, existing_op, readable) existing_kind = _attr_value(readable, GEN_AI_SPAN_KIND) + if ( isinstance(existing_kind, str) and existing_kind @@ -819,10 +836,11 @@ def _apply_semantic_attributes( ): span_kind = existing_kind - # 1) gen_ai.span.kind (only set if not already present) + # 1) gen_ai.span.kind (only set if not already present, or when a + # later-written MAF attribute lets us refine our own WORKFLOW + # fallback into a more specific kind). if not existing_kind or ( - existing_kind != span_kind - and existing_kind == GenAISpanKind.WORKFLOW + existing_kind == GenAISpanKind.WORKFLOW and span_kind != GenAISpanKind.WORKFLOW ): _set_attr(live, GEN_AI_SPAN_KIND, span_kind) @@ -834,13 +852,10 @@ def _apply_semantic_attributes( # ``observability.py:2101``) we also override when our # classification disagrees, provided the span is one of the # kinds whose operation.name we own (AGENT reclassification of - # ``executor.process``, plus MCP logical spans). + # ``executor.process``). if not existing_op: _set_attr(live, GEN_AI_OPERATION_NAME, op_name) - elif existing_op != op_name and span_kind in { - GenAISpanKind.AGENT, - GenAISpanKind.MCP, - }: + elif existing_op != op_name and span_kind == GenAISpanKind.AGENT: _set_attr(live, GEN_AI_OPERATION_NAME, op_name) if span_kind == GenAISpanKind.MCP and not _attr_value( diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/util_genai_bridge.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/util_genai_bridge.py index fb58419b7..d86b29d42 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/util_genai_bridge.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/util_genai_bridge.py @@ -392,9 +392,11 @@ def _create_mcp_client_span( attributes: dict[str, Any] | None = None, ) -> Generator[OtelSpan, Any, Any]: bridge_attrs = dict(attributes or {}) - bridge_attrs[GEN_AI_OPERATION_NAME] = GenAIOperation.MCP - bridge_attrs[GEN_AI_SPAN_KIND] = GenAISpanKind.MCP - bridge_attrs.setdefault("gen_ai.tool.name", target or method_name) + if method_name == "tools/call": + bridge_attrs[GEN_AI_OPERATION_NAME] = GenAIOperation.EXECUTE_TOOL + bridge_attrs[GEN_AI_SPAN_KIND] = GenAISpanKind.MCP + if target: + bridge_attrs.setdefault("gen_ai.tool.name", target) with original(method_name, target, bridge_attrs) as span: yield span @@ -514,17 +516,15 @@ def _prepare_start_attributes(attributes: Mapping[Any, Any]) -> dict[str, Any]: bridge_attrs = dict(attributes) op_name = _mapping_value(bridge_attrs, GEN_AI_OPERATION_NAME) span_name = _span_name_from_attributes(bridge_attrs) - span_kind, classified_op = _classify_span( - span_name, op_name if isinstance(op_name, str) else None, bridge_attrs - ) if not _is_maf_span( span_name, op_name if isinstance(op_name, str) else None, bridge_attrs ): return bridge_attrs + span_kind, classified_op = _classify_span( + span_name, op_name if isinstance(op_name, str) else None, bridge_attrs + ) if not _mapping_value(bridge_attrs, GEN_AI_OPERATION_NAME): bridge_attrs[GEN_AI_OPERATION_NAME] = classified_op - elif span_kind == GenAISpanKind.MCP: - bridge_attrs[GEN_AI_OPERATION_NAME] = classified_op if not _mapping_value(bridge_attrs, GEN_AI_SPAN_KIND): bridge_attrs[GEN_AI_SPAN_KIND] = span_kind provider = _normalize_provider( @@ -555,7 +555,10 @@ def _finalize_with_util_genai(span: OtelSpan) -> None: span, GEN_AI_RESPONSE_TTFT ): span.set_attribute(GEN_AI_RESPONSE_TTFT, ttft) - elif span_kind == GenAISpanKind.AGENT: + elif ( + span_kind == GenAISpanKind.AGENT + and op_name == GenAIOperation.INVOKE_AGENT + ): _apply_invoke_agent_finish_attributes( span, _invoke_agent_invocation(span) ) @@ -584,12 +587,12 @@ def _set_common_live_attributes( current_op = _attr_value(span, GEN_AI_OPERATION_NAME) if not current_op or ( current_op != op_name - and span_kind - in {GenAISpanKind.AGENT, GenAISpanKind.MCP} + and span_kind == GenAISpanKind.AGENT ): span.set_attribute(GEN_AI_OPERATION_NAME, op_name) - if span_kind == GenAISpanKind.MCP and not _attr_value( - span, "gen_ai.tool.name" + if ( + span_kind == GenAISpanKind.MCP + and not _attr_value(span, "gen_ai.tool.name") ): tool_name = _mcp_tool_name(getattr(span, "name", "") or "", span) if tool_name: diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_processor.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_processor.py index 0ad1a5b87..f3c4eb12f 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_processor.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_processor.py @@ -165,7 +165,7 @@ def test_workflow_start_attrs_are_exported_if_exporter_runs_first(): assert "gen_ai.workflow.id" not in s.attributes -def test_executor_process_function_executor_stays_workflow(): +def test_executor_process_function_executor_stays_workflow_operation(): tp, tracer, exporter, _ = _setup() with tracer.start_as_current_span("executor.process fid-1") as span: # MAF does NOT set gen_ai.operation.name for executor.process spans. @@ -209,7 +209,7 @@ def test_executor_process_agent_executor_becomes_agent(): ) -def test_executor_process_unknown_executor_stays_workflow(): +def test_executor_process_unknown_executor_stays_workflow_operation(): tp, tracer, exporter, _ = _setup() with tracer.start_as_current_span("executor.process xid") as span: span.set_attribute("executor.id", "xid") @@ -435,7 +435,7 @@ def test_safe_dumps_truncates_at_4kb(): assert len(out) <= 4096 -def test_mcp_span_classified_as_mcp_execute_tool(): +def test_mcp_tool_call_span_classified_as_mcp_execute_tool(): """MCP spans emitted by MAF's ``create_mcp_client_span`` carry no ``gen_ai.operation.name``; their name is ``{mcp.method.name} {target}`` (unbounded), so they must be detected via the ``mcp.method.name`` @@ -453,25 +453,25 @@ def test_mcp_span_classified_as_mcp_execute_tool(): spans = _flush(exporter) s = spans[0] assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.MCP - assert s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.MCP + assert s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.EXECUTE_TOOL assert s.attributes.get("gen_ai.tool.name") == "get_weather" -def test_mcp_span_via_client_kind_and_mcp_attr_fallback(): - """Fallback path: a CLIENT span with any ``mcp.*`` attribute (but missing - ``mcp.method.name``) is still classified as MCP.""" +def test_mcp_lifecycle_span_is_not_classified_as_genai(): + """MCP protocol lifecycle spans are not GenAI tool executions.""" from opentelemetry.trace import SpanKind tp, tracer, exporter, _ = _setup() with tracer.start_as_current_span( "initialize", kind=SpanKind.CLIENT ) as span: + span.set_attribute("mcp.method.name", "initialize") span.set_attribute("mcp.protocol.version", "2024-11-05") spans = _flush(exporter) s = spans[0] - assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.MCP - assert s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.MCP - assert s.attributes.get("gen_ai.tool.name") == "initialize" + assert GEN_AI_SPAN_KIND not in s.attributes + assert GEN_AI_OPERATION_NAME not in s.attributes + assert s.attributes.get("mcp.method.name") == "initialize" def test_non_mcp_client_span_is_not_misclassified_as_mcp(): @@ -490,16 +490,14 @@ def test_non_mcp_client_span_is_not_misclassified_as_mcp(): assert GEN_AI_OPERATION_NAME not in s.attributes -def test_mcp_span_kind_set_when_maf_writes_execute_tool(): +def test_mcp_tool_call_stays_mcp_when_maf_writes_execute_tool(): """[P1] regression: MAF emits ``gen_ai.operation.name=execute_tool`` on the MCP ``tools/call`` inner span (its ``create_mcp_client_span`` reuses the tool-call op name even though it sets ``mcp.method.name``). The processor - must set the logical span kind to ``MCP`` so the span is not mislabeled as a - plain TOOL call in the ARMS pipeline. + must keep the logical span kind as ``MCP`` so it is consistent with the + LoongSuite GenAI registry while still using upstream ``execute_tool``. - Before the fix, MCP spans were identified with a non-registry operation - value. The current LoongSuite profile uses ``MCP`` as span kind and - ``execute_tool`` as operation name. + Non-tool MCP lifecycle spans are intentionally left as protocol spans. """ from opentelemetry.trace import SpanKind @@ -513,7 +511,7 @@ def test_mcp_span_kind_set_when_maf_writes_execute_tool(): spans = _flush(exporter) s = spans[0] assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.MCP - assert s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.MCP + assert s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.EXECUTE_TOOL assert s.attributes.get("gen_ai.tool.name") == "slow_summary" diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_util_genai_bridge.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_util_genai_bridge.py index 38b7b26c7..84916916e 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_util_genai_bridge.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_util_genai_bridge.py @@ -315,10 +315,27 @@ def test_mcp_span_is_seeded_before_export(monkeypatch): span = exporter.get_finished_spans()[0] assert span.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.MCP - assert span.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.MCP + assert span.attributes.get(GEN_AI_OPERATION_NAME) == ( + GenAIOperation.EXECUTE_TOOL + ) assert span.attributes.get("gen_ai.tool.name") == "city_score" +def test_mcp_lifecycle_span_is_not_seeded_as_genai(monkeypatch): + _, exporter = _install_fake_observability(monkeypatch) + util_genai_bridge.apply_util_genai_bridge() + try: + mcp_mod = sys.modules["agent_framework._mcp"] + with mcp_mod.create_mcp_client_span("initialize"): + pass + finally: + util_genai_bridge.revert_util_genai_bridge() + span = exporter.get_finished_spans()[-1] + assert span.attributes.get("mcp.method.name") == "initialize" + assert GEN_AI_SPAN_KIND not in span.attributes + assert GEN_AI_OPERATION_NAME not in span.attributes + + def test_apply_revert_apply_keeps_single_wrapper_layer(monkeypatch): obs_mod, _ = _install_fake_observability(monkeypatch) original_get_span = obs_mod._get_span diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/extended_semconv/gen_ai_extended_attributes.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/extended_semconv/gen_ai_extended_attributes.py index 8aca568f7..b471e8a66 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/extended_semconv/gen_ai_extended_attributes.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/extended_semconv/gen_ai_extended_attributes.py @@ -210,6 +210,12 @@ class GenAiSpanKindValues(Enum): TOOL = "TOOL" """Tool execution operation.""" + WORKFLOW = "WORKFLOW" + """GenAI workflow invocation operation.""" + + MCP = "MCP" + """Tool execution via Model Context Protocol.""" + RETRIEVER = "RETRIEVER" """Document retrieval operation.""" From de7e88c66c08ef25f8a62d56748b47e055675c59 Mon Sep 17 00:00:00 2001 From: sipercai <53717475+sipercai@users.noreply.github.com> Date: Tue, 30 Jun 2026 10:54:39 +0800 Subject: [PATCH 14/21] style(maf): apply ruff formatting --- .../microsoft_agent_framework/util_genai_bridge.py | 8 +++----- .../tests/test_processor.py | 8 ++++++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/util_genai_bridge.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/util_genai_bridge.py index d86b29d42..e152501dd 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/util_genai_bridge.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/util_genai_bridge.py @@ -586,13 +586,11 @@ def _set_common_live_attributes( span.set_attribute(GEN_AI_SPAN_KIND, span_kind) current_op = _attr_value(span, GEN_AI_OPERATION_NAME) if not current_op or ( - current_op != op_name - and span_kind == GenAISpanKind.AGENT + current_op != op_name and span_kind == GenAISpanKind.AGENT ): span.set_attribute(GEN_AI_OPERATION_NAME, op_name) - if ( - span_kind == GenAISpanKind.MCP - and not _attr_value(span, "gen_ai.tool.name") + if span_kind == GenAISpanKind.MCP and not _attr_value( + span, "gen_ai.tool.name" ): tool_name = _mcp_tool_name(getattr(span, "name", "") or "", span) if tool_name: diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_processor.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_processor.py index f3c4eb12f..ffa7c4dcd 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_processor.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_processor.py @@ -453,7 +453,9 @@ def test_mcp_tool_call_span_classified_as_mcp_execute_tool(): spans = _flush(exporter) s = spans[0] assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.MCP - assert s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.EXECUTE_TOOL + assert ( + s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.EXECUTE_TOOL + ) assert s.attributes.get("gen_ai.tool.name") == "get_weather" @@ -511,7 +513,9 @@ def test_mcp_tool_call_stays_mcp_when_maf_writes_execute_tool(): spans = _flush(exporter) s = spans[0] assert s.attributes.get(GEN_AI_SPAN_KIND) == GenAISpanKind.MCP - assert s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.EXECUTE_TOOL + assert ( + s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.EXECUTE_TOOL + ) assert s.attributes.get("gen_ai.tool.name") == "slow_summary" From 385a33d9113a8308b94093f807d7bc8b04cbb712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=81=E5=B1=BF?= Date: Tue, 2 Jun 2026 17:34:50 +0800 Subject: [PATCH 15/21] feat(agentscope): support AgentScope v2 --- .../CHANGELOG.md | 5 + .../pyproject.toml | 7 +- .../instrumentation/agentscope/__init__.py | 121 ++++- .../agentscope/_v2_middleware.py | 463 ++++++++++++++++++ .../instrumentation/agentscope/package.py | 27 +- .../test_v2_agent_concurrent_e2e.yaml | 242 +++++++++ .../test_v2_agent_non_streaming_e2e.yaml | 122 +++++ .../test_v2_agent_streaming_e2e.yaml | 111 +++++ .../tests/conftest.py | 4 +- .../tests/requirements.latest.txt | 6 +- .../tests/requirements.oldest.txt | 6 +- .../tests/requirements.v2.txt | 31 ++ .../tests/test_v2_instrumentation.py | 305 ++++++++++++ tox-loongsuite.ini | 6 + 14 files changed, 1430 insertions(+), 26 deletions(-) create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/_v2_middleware.py create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/cassettes/test_v2_agent_concurrent_e2e.yaml create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/cassettes/test_v2_agent_non_streaming_e2e.yaml create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/cassettes/test_v2_agent_streaming_e2e.yaml create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/requirements.v2.txt create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/test_v2_instrumentation.py diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/CHANGELOG.md b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/CHANGELOG.md index 2627bd769..bd834a3cc 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/CHANGELOG.md +++ b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Add version-aware AgentScope v2 middleware instrumentation while preserving + AgentScope v1 compatibility. + ## Version 0.6.0 (2026-06-03) There are no changelog entries for this release. diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/pyproject.toml b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/pyproject.toml index a6749333e..a5d720e93 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/pyproject.toml +++ b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/pyproject.toml @@ -30,12 +30,13 @@ dependencies = [ "opentelemetry-instrumentation >= 0.58b0", "opentelemetry-semantic-conventions >= 0.58b0", "opentelemetry-util-genai", + "packaging >= 18.0", "wrapt >= 1.17.3, < 2.0.0", ] [project.optional-dependencies] instruments = [ - "agentscope >= 1.0.0", + "agentscope >= 1.0.0, < 3.0.0", ] test = [ @@ -43,9 +44,9 @@ test = [ "pytest-asyncio ~= 0.23.0", "pytest-cov ~= 4.1.0", "pytest-vcr ~= 1.0.2", - "vcrpy ~= 5.1.0", + "vcrpy >= 8.1.1", "pyyaml ~= 6.0", - "agentscope >= 1.0.0", + "agentscope >= 1.0.0, < 3.0.0", ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/__init__.py b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/__init__.py index bb210120d..2002bd3f9 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/__init__.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/__init__.py @@ -13,7 +13,7 @@ # limitations under the License. """ -AgentScope instrumentation supporting `agentscope >= 1.0.0`. +AgentScope instrumentation supporting `agentscope >= 1.0.0, < 3.0.0`. Usage ----- @@ -49,25 +49,22 @@ async def call_model(): from __future__ import annotations import logging +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version as metadata_version from typing import Any, Collection from wrapt import wrap_function_wrapper from opentelemetry import trace as trace_api -from opentelemetry.instrumentation.agentscope.package import _instruments +from opentelemetry.instrumentation.agentscope.package import ( + get_installed_instrumentation_dependencies, +) from opentelemetry.instrumentation.agentscope.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import unwrap from opentelemetry.semconv.schemas import Schemas from opentelemetry.util.genai.extended_handler import ExtendedTelemetryHandler -from ._wrapper import ( - AgentScopeAgentWrapper, - AgentScopeChatModelWrapper, - AgentScopeEmbeddingModelWrapper, -) -from .patch import wrap_formatter_format, wrap_tool_call - logger = logging.getLogger(__name__) _MODEL_MODULE = "agentscope.model" @@ -94,9 +91,10 @@ def __init__(self): self._handler = ( None # ExtendedTelemetryHandler handles all other operations ) + self._agentscope_major = None def instrumentation_dependencies(self) -> Collection[str]: - return _instruments + return get_installed_instrumentation_dependencies() def _setup_tracing_patch(self, wrapped, instance, args, kwargs): """Replace setup_tracing with no-op to use OTEL instead.""" @@ -127,6 +125,23 @@ def _instrument(self, **kwargs: Any) -> None: schema_url=Schemas.V1_37_0.value, ) + self._agentscope_major = _get_agentscope_major() + if self._agentscope_major >= 2: + self._instrument_v2() + else: + self._instrument_v1() + + def _instrument_v1(self) -> None: + from ._wrapper import ( # noqa: PLC0415 + AgentScopeAgentWrapper, + AgentScopeChatModelWrapper, + AgentScopeEmbeddingModelWrapper, + ) + from .patch import ( # noqa: PLC0415 + wrap_formatter_format, + wrap_tool_call, + ) + # Instrument ChatModelBase try: chat_wrapper = AgentScopeChatModelWrapper(handler=self._handler) @@ -224,21 +239,48 @@ def wrap_formatter_with_tracer(wrapped, instance, args, kwargs): def _uninstrument(self, **kwargs: Any) -> None: """Disable AgentScope instrumentation.""" + del kwargs + if self._agentscope_major is None: + self._agentscope_major = _get_agentscope_major() + if self._agentscope_major >= 2: + self._uninstrument_v2() + else: + self._uninstrument_v1() + self._handler = None + self._tracer = None + self._agentscope_major = None + + def _uninstrument_v1(self) -> None: try: - AgentScopeChatModelWrapper.restore_original_methods() - logger.debug("Restored ChatModelBase methods") + from ._wrapper import ( # noqa: PLC0415 + AgentScopeAgentWrapper, + AgentScopeChatModelWrapper, + AgentScopeEmbeddingModelWrapper, + ) + except Exception as e: + logger.warning(f"Failed to import AgentScope wrappers: {e}") + AgentScopeAgentWrapper = None + AgentScopeChatModelWrapper = None + AgentScopeEmbeddingModelWrapper = None + + try: + if AgentScopeChatModelWrapper is not None: + AgentScopeChatModelWrapper.restore_original_methods() + logger.debug("Restored ChatModelBase methods") except Exception as e: logger.warning(f"Failed to restore ChatModelBase: {e}") try: - AgentScopeAgentWrapper.restore_original_methods() - logger.debug("Restored AgentBase methods") + if AgentScopeAgentWrapper is not None: + AgentScopeAgentWrapper.restore_original_methods() + logger.debug("Restored AgentBase methods") except Exception as e: logger.warning(f"Failed to restore AgentBase: {e}") try: - AgentScopeEmbeddingModelWrapper.restore_original_methods() - logger.debug("Restored EmbeddingModelBase methods") + if AgentScopeEmbeddingModelWrapper is not None: + AgentScopeEmbeddingModelWrapper.restore_original_methods() + logger.debug("Restored EmbeddingModelBase methods") except Exception as e: logger.warning(f"Failed to restore EmbeddingModelBase: {e}") @@ -301,3 +343,50 @@ def _uninstrument(self, **kwargs: Any) -> None: logger.warning( f"Failed to uninstrument _check_tracing_enabled: {e}" ) + + def _instrument_v2(self) -> None: + from ._v2_middleware import ( # noqa: PLC0415 + AgentScopeV2Middleware, + append_loongsuite_middleware, + ) + + try: + + def wrap_agent_init(wrapped, instance, args, kwargs): + args, kwargs = append_loongsuite_middleware( + args, + kwargs, + AgentScopeV2Middleware(handler=lambda: self._handler), + ) + return wrapped(*args, **kwargs) + + wrap_function_wrapper( + module=_AGENT_MODULE, + name="Agent.__init__", + wrapper=wrap_agent_init, + ) + logger.debug("Instrumented AgentScope v2 Agent") + except Exception as e: + logger.warning(f"Failed to instrument AgentScope v2 Agent: {e}") + + def _uninstrument_v2(self) -> None: + try: + import agentscope.agent # noqa: PLC0415 + + unwrap(agentscope.agent.Agent, "__init__") + logger.debug("Uninstrumented AgentScope v2 Agent") + except Exception as e: + logger.warning(f"Failed to uninstrument AgentScope v2 Agent: {e}") + + +def _get_agentscope_major() -> int: + try: + installed_version = metadata_version("agentscope") + except PackageNotFoundError: + return 1 + + major_text = installed_version.split(".", 1)[0] + try: + return int(major_text) + except ValueError: + return 1 diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/_v2_middleware.py b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/_v2_middleware.py new file mode 100644 index 000000000..2920490d3 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/_v2_middleware.py @@ -0,0 +1,463 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""AgentScope v2 middleware instrumentation.""" + +from __future__ import annotations + +import inspect +import json +import logging +import timeit +from collections.abc import AsyncGenerator, Awaitable, Callable, Sequence +from typing import Any + +from agentscope.agent import Agent +from agentscope.message import Msg +from agentscope.middleware import MiddlewareBase +from agentscope.model import ChatModelBase, ChatResponse +from agentscope.tool import ToolResponse + +from opentelemetry.context import Context, get_current +from opentelemetry.util.genai.extended_handler import ExtendedTelemetryHandler +from opentelemetry.util.genai.extended_types import ( + ExecuteToolInvocation, + InvokeAgentInvocation, +) +from opentelemetry.util.genai.types import ( + Error, + FunctionToolDefinition, + InputMessage, + LLMInvocation, + OutputMessage, + Reasoning, + Text, + ToolCall, + ToolCallResponse, +) + +logger = logging.getLogger(__name__) + +_MIDDLEWARE_PARAMETER = "middlewares" +_FIRST_TOKEN_EVENT_TYPES = { + "text_block_delta", + "thinking_block_delta", + "tool_call_delta", +} + + +def append_loongsuite_middleware( + args: tuple[Any, ...], + kwargs: dict[str, Any], + middleware: "AgentScopeV2Middleware", +) -> tuple[tuple[Any, ...], dict[str, Any]]: + """Append LoongSuite middleware to AgentScope v2 Agent.__init__ inputs.""" + if _MIDDLEWARE_PARAMETER in kwargs: + kwargs = dict(kwargs) + kwargs[_MIDDLEWARE_PARAMETER] = _append_once( + kwargs.get(_MIDDLEWARE_PARAMETER), middleware + ) + return args, kwargs + + middleware_position = _middleware_arg_position() + if middleware_position is not None and len(args) > middleware_position: + updated_args = list(args) + updated_args[middleware_position] = _append_once( + updated_args[middleware_position], + middleware, + ) + return tuple(updated_args), kwargs + + kwargs = dict(kwargs) + kwargs[_MIDDLEWARE_PARAMETER] = [middleware] + return args, kwargs + + +def _append_once( + middlewares: Sequence[MiddlewareBase] | None, + middleware: "AgentScopeV2Middleware", +) -> list[MiddlewareBase]: + result = list(middlewares or []) + if any(isinstance(item, AgentScopeV2Middleware) for item in result): + return result + result.append(middleware) + return result + + +class AgentScopeV2Middleware(MiddlewareBase): + """LoongSuite telemetry adapter for AgentScope v2 middleware hooks.""" + + def __init__( + self, handler: Callable[[], ExtendedTelemetryHandler | None] + ) -> None: + self._handler = handler + + async def on_reply( + self, + agent: Agent, + input_kwargs: dict, + next_handler: Callable[..., AsyncGenerator], + ) -> AsyncGenerator: + handler = self._handler() + if handler is None: + async for item in next_handler(**input_kwargs): + yield item + return + + invocation = _create_agent_invocation(agent, input_kwargs) + handler.start_invoke_agent(invocation) + first_token_seen = False + last_msg = None + closed = False + try: + async for item in next_handler(**input_kwargs): + if not first_token_seen and _is_first_token_event(item): + invocation.monotonic_first_token_s = timeit.default_timer() + first_token_seen = True + if isinstance(item, Msg): + last_msg = item + yield item + except BaseException as exc: + handler.fail_invoke_agent( + invocation, + Error(message=str(exc) or type(exc).__name__, type=type(exc)), + ) + closed = True + raise + else: + if last_msg is not None: + invocation.output_messages = [_message_to_output(last_msg)] + if last_msg.usage is not None: + invocation.input_tokens = last_msg.usage.input_tokens + invocation.output_tokens = last_msg.usage.output_tokens + handler.stop_invoke_agent(invocation) + closed = True + finally: + if not closed: + handler.stop_invoke_agent(invocation) + + async def on_model_call( + self, + agent: Agent, + input_kwargs: dict, + next_handler: Callable[ + ..., + Awaitable[ChatResponse | AsyncGenerator[ChatResponse, None]], + ], + ) -> ChatResponse | AsyncGenerator[ChatResponse, None]: + model = input_kwargs.get("current_model") + if not isinstance(model, ChatModelBase): + return await next_handler(**input_kwargs) + + handler = self._handler() + if handler is None: + return await next_handler(**input_kwargs) + + invocation = _create_llm_invocation(model, input_kwargs) + span_context = get_current() + started = False + if not _is_streaming_model(model, input_kwargs): + handler.start_llm(invocation, context=span_context) + started = True + try: + result = await next_handler(**input_kwargs) + if inspect.isasyncgen(result): + return self._wrap_model_stream( + result, + invocation, + span_context, + handler, + span_started=started, + ) + + if not started: + handler.start_llm(invocation, context=span_context) + started = True + _finish_llm_invocation(invocation, result) + handler.stop_llm(invocation) + return result + except BaseException as exc: + if not started: + handler.start_llm(invocation, context=span_context) + handler.fail_llm( + invocation, + Error(message=str(exc) or type(exc).__name__, type=type(exc)), + ) + raise + + async def _wrap_model_stream( + self, + result: AsyncGenerator[ChatResponse, None], + invocation: LLMInvocation, + span_context: Context, + handler: ExtendedTelemetryHandler, + *, + span_started: bool, + ) -> AsyncGenerator[ChatResponse, None]: + first_token_seen = False + last_chunk = None + closed = False + if not span_started: + handler.start_llm(invocation, context=span_context) + span_started = True + try: + async for chunk in result: + if not first_token_seen: + invocation.monotonic_first_token_s = timeit.default_timer() + first_token_seen = True + last_chunk = chunk + yield chunk + except BaseException as exc: + handler.fail_llm( + invocation, + Error(message=str(exc) or type(exc).__name__, type=type(exc)), + ) + closed = True + raise + else: + _finish_llm_invocation(invocation, last_chunk) + handler.stop_llm(invocation) + closed = True + finally: + if span_started and not closed: + handler.stop_llm(invocation) + + async def on_acting( + self, + agent: Agent, + input_kwargs: dict, + next_handler: Callable[..., AsyncGenerator], + ) -> AsyncGenerator: + handler = self._handler() + if handler is None: + async for item in next_handler(**input_kwargs): + yield item + return + + tool_call = input_kwargs.get("tool_call") + invocation = ExecuteToolInvocation( + tool_name=getattr(tool_call, "name", "unknown_tool"), + tool_call_id=getattr(tool_call, "id", None), + tool_call_arguments=_loads_json(getattr(tool_call, "input", None)), + provider="agentscope", + ) + handler.start_execute_tool(invocation) + last_item = None + closed = False + try: + async for item in next_handler(**input_kwargs): + last_item = item + yield item + except BaseException as exc: + handler.fail_execute_tool( + invocation, + Error(message=str(exc) or type(exc).__name__, type=type(exc)), + ) + closed = True + raise + else: + if isinstance(last_item, ToolResponse): + invocation.tool_call_result = last_item.content + elif last_item is not None: + invocation.tool_call_result = str(last_item) + handler.stop_execute_tool(invocation) + closed = True + finally: + if not closed: + handler.stop_execute_tool(invocation) + + +def _create_agent_invocation( + agent: Agent, + input_kwargs: dict[str, Any], +) -> InvokeAgentInvocation: + model = getattr(agent, "model", None) + request_model = getattr(model, "model", None) + provider = _get_provider_name(model) + inputs = input_kwargs.get("inputs") + return InvokeAgentInvocation( + provider=provider, + agent_name=getattr(agent, "name", "unknown_agent"), + agent_id=getattr(getattr(agent, "state", None), "session_id", None), + conversation_id=getattr(getattr(agent, "state", None), "session_id", None), + request_model=request_model, + input_messages=_messages_to_inputs(inputs), + system_instruction=[Text(content=getattr(agent, "_system_prompt", ""))], + ) + + +def _create_llm_invocation( + model: ChatModelBase, + input_kwargs: dict[str, Any], +) -> LLMInvocation: + invocation = LLMInvocation( + request_model=getattr(model, "model", None), + provider=_get_provider_name(model), + input_messages=_messages_to_inputs(input_kwargs.get("messages")), + tool_definitions=_tool_definitions(input_kwargs.get("tools")), + ) + parameters = getattr(model, "parameters", None) + for source in (parameters, input_kwargs): + _set_if_present(invocation, "temperature", source) + _set_if_present(invocation, "top_p", source) + _set_if_present(invocation, "max_tokens", source) + return invocation + + +def _finish_llm_invocation( + invocation: LLMInvocation, + response: ChatResponse | None, +) -> None: + if response is None: + return + invocation.response_id = getattr(response, "id", None) + invocation.output_messages = [_chat_response_to_output(response)] + usage = getattr(response, "usage", None) + if usage is not None: + invocation.input_tokens = getattr(usage, "input_tokens", None) + invocation.output_tokens = getattr(usage, "output_tokens", None) + + +def _messages_to_inputs(value: Any) -> list[InputMessage]: + if value is None: + return [] + if isinstance(value, Msg): + return [_message_to_input(value)] + if isinstance(value, list): + return [_message_to_input(item) for item in value if isinstance(item, Msg)] + return [] + + +def _message_to_input(msg: Msg) -> InputMessage: + return InputMessage(role=msg.role, parts=_blocks_to_parts(msg.content)) + + +def _message_to_output(msg: Msg) -> OutputMessage: + return OutputMessage( + role=msg.role, + parts=_blocks_to_parts(msg.content), + finish_reason="stop", + ) + + +def _chat_response_to_output(response: ChatResponse) -> OutputMessage: + finish_reason = "stop" + if any(getattr(block, "type", None) == "tool_call" for block in response.content): + finish_reason = "tool_calls" + return OutputMessage( + role="assistant", + parts=_blocks_to_parts(response.content), + finish_reason=finish_reason, + ) + + +def _blocks_to_parts(blocks: Sequence[Any]) -> list[Any]: + parts = [] + for block in blocks: + block_type = getattr(block, "type", None) + if block_type == "text": + parts.append(Text(content=getattr(block, "text", ""))) + elif block_type == "thinking": + parts.append(Reasoning(content=getattr(block, "thinking", ""))) + elif block_type == "tool_call": + parts.append( + ToolCall( + id=getattr(block, "id", None), + name=getattr(block, "name", ""), + arguments=_loads_json(getattr(block, "input", None)), + ) + ) + elif block_type == "tool_result": + parts.append( + ToolCallResponse( + id=getattr(block, "id", None), + response=getattr(block, "output", ""), + ) + ) + return parts + + +def _tool_definitions(tools: list[dict[str, Any]] | None) -> list[Any]: + if not tools: + return [] + definitions = [] + for tool in tools: + function = tool.get("function") if isinstance(tool, dict) else None + if not isinstance(function, dict): + continue + definitions.append( + FunctionToolDefinition( + name=function.get("name", ""), + description=function.get("description"), + parameters=function.get("parameters"), + ) + ) + return definitions + + +def _get_provider_name(model: Any) -> str: + class_name = model.__class__.__name__.lower() if model is not None else "" + if "dashscope" in class_name: + return "dashscope" + if "openai" in class_name: + return "openai" + if "anthropic" in class_name: + return "anthropic" + if "gemini" in class_name: + return "gcp.gen_ai" + if "ollama" in class_name: + return "ollama" + return "agentscope" + + +def _is_first_token_event(item: Any) -> bool: + event_type = getattr(item, "type", None) + return event_type in _FIRST_TOKEN_EVENT_TYPES + + +def _middleware_arg_position() -> int | None: + try: + parameters = list(inspect.signature(Agent.__init__).parameters) + return parameters.index(_MIDDLEWARE_PARAMETER) - 1 + except (TypeError, ValueError): + return None + + +def _is_streaming_model(model: ChatModelBase, input_kwargs: dict[str, Any]) -> bool: + if "stream" in input_kwargs: + return bool(input_kwargs["stream"]) + return bool(getattr(model, "stream", False)) + + +def _loads_json(value: Any) -> Any: + if not isinstance(value, str): + return value + try: + return json.loads(value) + except ValueError: + return value + + +def _set_if_present( + invocation: LLMInvocation, + field_name: str, + source: Any, +) -> None: + value = ( + source.get(field_name) + if isinstance(source, dict) + else getattr(source, field_name, None) + ) + if value is not None: + setattr(invocation, field_name, value) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/package.py b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/package.py index c140bd607..6bbe8643b 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/package.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/package.py @@ -12,6 +12,31 @@ # See the License for the specific language governing permissions and # limitations under the License. -_instruments = ("agentscope >= 1.0.0",) +from __future__ import annotations + +from importlib.metadata import PackageNotFoundError, version + +from packaging.requirements import Requirement + +_instruments_v1 = ("agentscope >= 1.0.0, < 2.0.0",) +_instruments_v2 = ("agentscope >= 2.0.0, < 3.0.0",) +_instruments = ("agentscope >= 1.0.0, < 3.0.0",) _supports_metrics = False + + +def get_installed_instrumentation_dependencies(): + """Return the AgentScope dependency range matching the installed major.""" + try: + installed_version = version("agentscope") + except PackageNotFoundError: + return _instruments + + for requirement in (_instruments_v2[0], _instruments_v1[0]): + if Requirement(requirement).specifier.contains( + installed_version, + prereleases=True, + ): + return (requirement,) + + return _instruments diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/cassettes/test_v2_agent_concurrent_e2e.yaml b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/cassettes/test_v2_agent_concurrent_e2e.yaml new file mode 100644 index 000000000..fcf53c5b7 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/cassettes/test_v2_agent_concurrent_e2e.yaml @@ -0,0 +1,242 @@ +interactions: +- request: + body: |- + { + "messages": [ + { + "role": "system", + "content": [ + { + "type": "text", + "text": "Reply with exactly one short sentence." + } + ] + }, + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Say OK for request 2." + } + ] + } + ], + "model": "qwen-plus", + "max_tokens": 16, + "stream": false, + "enable_thinking": false + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '258' + Content-Type: + - application/json + Host: + - dashscope.aliyuncs.com + User-Agent: + - AsyncOpenAI/Python 2.40.0 + X-Stainless-Arch: + - arm64 + X-Stainless-Async: + - async:asyncio + X-Stainless-Lang: + - python + X-Stainless-OS: + - MacOS + X-Stainless-Package-Version: + - 2.40.0 + X-Stainless-Runtime: + - CPython + X-Stainless-Runtime-Version: + - 3.11.13 + authorization: + - + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + method: POST + uri: https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions + response: + body: + string: |- + { + "model": "qwen-plus", + "id": "chatcmpl-0d58865c-c0c4-97df-9ef5-841029ada056", + "choices": [ + { + "message": { + "content": "OK", + "role": "assistant" + }, + "index": 0, + "finish_reason": "stop" + } + ], + "created": 1780388664, + "object": "chat.completion", + "usage": { + "total_tokens": 28, + "completion_tokens": 1, + "prompt_tokens": 27, + "prompt_tokens_details": { + "cached_tokens": 0 + } + } + } + headers: + content-length: + - '328' + content-type: + - application/json + date: + - Tue, 02 Jun 2026 08:24:24 GMT + req-arrive-time: + - '1780388664520' + req-cost-time: + - '378' + resp-start-time: + - '1780388664899' + server: + - istio-envoy + transfer-encoding: + - chunked + vary: + - Origin,Access-Control-Request-Method,Access-Control-Request-Headers, Accept-Encoding + x-dashscope-call-gateway: + - 'true' + x-envoy-upstream-service-time: + - '378' + x-request-id: + - 0d58865c-c0c4-97df-9ef5-841029ada056 + status: + code: 200 + message: OK +- request: + body: |- + { + "messages": [ + { + "role": "system", + "content": [ + { + "type": "text", + "text": "Reply with exactly one short sentence." + } + ] + }, + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Say OK for request 1." + } + ] + } + ], + "model": "qwen-plus", + "max_tokens": 16, + "stream": false, + "enable_thinking": false + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '258' + Content-Type: + - application/json + Host: + - dashscope.aliyuncs.com + User-Agent: + - AsyncOpenAI/Python 2.40.0 + X-Stainless-Arch: + - arm64 + X-Stainless-Async: + - async:asyncio + X-Stainless-Lang: + - python + X-Stainless-OS: + - MacOS + X-Stainless-Package-Version: + - 2.40.0 + X-Stainless-Runtime: + - CPython + X-Stainless-Runtime-Version: + - 3.11.13 + authorization: + - + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + method: POST + uri: https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions + response: + body: + string: |- + { + "model": "qwen-plus", + "id": "chatcmpl-b1d9db3b-e07f-9dea-a72e-79a244bac8d2", + "choices": [ + { + "message": { + "content": "OK for request 1.", + "role": "assistant" + }, + "index": 0, + "finish_reason": "stop" + } + ], + "created": 1780388664, + "object": "chat.completion", + "usage": { + "total_tokens": 33, + "completion_tokens": 6, + "prompt_tokens": 27, + "prompt_tokens_details": { + "cached_tokens": 0 + } + } + } + headers: + content-length: + - '343' + content-type: + - application/json + date: + - Tue, 02 Jun 2026 08:24:24 GMT + req-arrive-time: + - '1780388664535' + req-cost-time: + - '510' + resp-start-time: + - '1780388665045' + server: + - istio-envoy + transfer-encoding: + - chunked + vary: + - Origin,Access-Control-Request-Method,Access-Control-Request-Headers, Accept-Encoding + x-dashscope-call-gateway: + - 'true' + x-envoy-upstream-service-time: + - '509' + x-request-id: + - b1d9db3b-e07f-9dea-a72e-79a244bac8d2 + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/cassettes/test_v2_agent_non_streaming_e2e.yaml b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/cassettes/test_v2_agent_non_streaming_e2e.yaml new file mode 100644 index 000000000..e6cb0678f --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/cassettes/test_v2_agent_non_streaming_e2e.yaml @@ -0,0 +1,122 @@ +interactions: +- request: + body: |- + { + "messages": [ + { + "role": "system", + "content": [ + { + "type": "text", + "text": "Reply with exactly: OK" + } + ] + }, + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Say OK." + } + ] + } + ], + "model": "qwen-plus", + "max_tokens": 16, + "stream": false, + "enable_thinking": false + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '228' + Content-Type: + - application/json + Host: + - dashscope.aliyuncs.com + User-Agent: + - AsyncOpenAI/Python 2.40.0 + X-Stainless-Arch: + - arm64 + X-Stainless-Async: + - async:asyncio + X-Stainless-Lang: + - python + X-Stainless-OS: + - MacOS + X-Stainless-Package-Version: + - 2.40.0 + X-Stainless-Runtime: + - CPython + X-Stainless-Runtime-Version: + - 3.11.13 + authorization: + - + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + method: POST + uri: https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions + response: + body: + string: |- + { + "model": "qwen-plus", + "id": "chatcmpl-d1fc4eda-fe73-9625-9108-2b83bca90dbb", + "choices": [ + { + "message": { + "content": "OK", + "role": "assistant" + }, + "index": 0, + "finish_reason": "stop" + } + ], + "created": 1780388663, + "object": "chat.completion", + "usage": { + "total_tokens": 22, + "completion_tokens": 1, + "prompt_tokens": 21, + "prompt_tokens_details": { + "cached_tokens": 0 + } + } + } + headers: + content-length: + - '328' + content-type: + - application/json + date: + - Tue, 02 Jun 2026 08:24:23 GMT + req-arrive-time: + - '1780388663479' + req-cost-time: + - '330' + resp-start-time: + - '1780388663810' + server: + - istio-envoy + transfer-encoding: + - chunked + vary: + - Origin,Access-Control-Request-Method,Access-Control-Request-Headers, Accept-Encoding + x-dashscope-call-gateway: + - 'true' + x-envoy-upstream-service-time: + - '330' + x-request-id: + - d1fc4eda-fe73-9625-9108-2b83bca90dbb + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/cassettes/test_v2_agent_streaming_e2e.yaml b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/cassettes/test_v2_agent_streaming_e2e.yaml new file mode 100644 index 000000000..36f3135a5 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/cassettes/test_v2_agent_streaming_e2e.yaml @@ -0,0 +1,111 @@ +interactions: +- request: + body: |- + { + "messages": [ + { + "role": "system", + "content": [ + { + "type": "text", + "text": "Reply with a short sentence." + } + ] + }, + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Say hello in one sentence." + } + ] + } + ], + "model": "qwen-plus", + "max_tokens": 16, + "stream": true, + "stream_options": { + "include_usage": true + }, + "enable_thinking": false + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '292' + Content-Type: + - application/json + Host: + - dashscope.aliyuncs.com + User-Agent: + - AsyncOpenAI/Python 2.40.0 + X-Stainless-Arch: + - arm64 + X-Stainless-Async: + - async:asyncio + X-Stainless-Lang: + - python + X-Stainless-OS: + - MacOS + X-Stainless-Package-Version: + - 2.40.0 + X-Stainless-Runtime: + - CPython + X-Stainless-Runtime-Version: + - 3.11.13 + authorization: + - + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + method: POST + uri: https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions + response: + body: + string: |+ + data: {"model":"qwen-plus","id":"chatcmpl-23d2afca-b6f8-9fb2-9c79-a40825fe928d","created":1780388664,"object":"chat.completion.chunk","usage":null,"choices":[{"logprobs":null,"index":0,"delta":{"content":"","role":"assistant"},"finish_reason":null}]} + + data: {"model":"qwen-plus","id":"chatcmpl-23d2afca-b6f8-9fb2-9c79-a40825fe928d","choices":[{"delta":{"content":"Hello"},"index":0,"finish_reason":null,"logprobs":null}],"created":1780388664,"object":"chat.completion.chunk","usage":null} + + data: {"model":"qwen-plus","id":"chatcmpl-23d2afca-b6f8-9fb2-9c79-a40825fe928d","choices":[{"delta":{"content":"!"},"index":0,"finish_reason":null,"logprobs":null}],"created":1780388664,"object":"chat.completion.chunk","usage":null} + + data: {"model":"qwen-plus","id":"chatcmpl-23d2afca-b6f8-9fb2-9c79-a40825fe928d","choices":[{"delta":{"content":""},"index":0,"finish_reason":"stop","logprobs":null}],"created":1780388664,"object":"chat.completion.chunk","usage":null} + + data: {"model":"qwen-plus","id":"chatcmpl-23d2afca-b6f8-9fb2-9c79-a40825fe928d","choices":[],"created":1780388664,"object":"chat.completion.chunk","usage":{"total_tokens":27,"completion_tokens":2,"prompt_tokens":25,"prompt_tokens_details":{"cached_tokens":0}}} + + data: [DONE] + + headers: + content-type: + - text/event-stream;charset=utf-8 + date: + - Tue, 02 Jun 2026 08:24:24 GMT + req-arrive-time: + - '1780388663952' + req-cost-time: + - '372' + resp-start-time: + - '1780388664324' + server: + - istio-envoy + transfer-encoding: + - chunked + vary: + - Origin + x-dashscope-call-gateway: + - 'true' + x-envoy-upstream-service-time: + - '372' + x-request-id: + - 23d2afca-b6f8-9fb2-9c79-a40825fe928d + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/conftest.py b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/conftest.py index 5cf7779d6..bd2976159 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/conftest.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/conftest.py @@ -245,8 +245,8 @@ def vcr_config(): """Configure VCR for recording and replaying HTTP requests""" return { "filter_headers": [ - ("authorization", "Bearer test_api_key"), - ("api-key", "test_api_key"), + ("authorization", ""), + ("api-key", ""), ], "decode_compressed_response": True, "before_record_response": scrub_response_headers, diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/requirements.latest.txt b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/requirements.latest.txt index 732bd8c57..d3cf39b4f 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/requirements.latest.txt +++ b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/requirements.latest.txt @@ -34,14 +34,16 @@ # This variant of the requirements aims to test the system using # the newest supported version of external dependencies. -agentscope>=1.0.0 +agentscope>=1.0.0,<2.0.0 pytest pytest-asyncio pytest-cov pytest-vcr>=1.0.2 -vcrpy>=5.1.0 +vcrpy>=8.1.1 aiohttp<3.12 pyyaml>=6.0 +packaging>=18.0 +aiohttp<3.9; python_version < "3.12" wrapt>=1.17.3,<2.0.0 -e opentelemetry-instrumentation diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/requirements.oldest.txt b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/requirements.oldest.txt index 63e71f573..6c2358ca2 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/requirements.oldest.txt +++ b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/requirements.oldest.txt @@ -15,14 +15,16 @@ # This variant of the requirements aims to test the system using # the oldest supported version of external dependencies. -agentscope>=1.0.0 +agentscope>=1.0.0,<2.0.0 pytest pytest-asyncio pytest-cov pytest-vcr>=1.0.2 -vcrpy>=5.1.0 +vcrpy>=8.1.1 aiohttp<3.12 pyyaml>=6.0 +packaging>=18.0 +aiohttp<3.9; python_version < "3.12" opentelemetry-api==1.37 opentelemetry-sdk==1.37 opentelemetry-instrumentation==0.58b0 diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/requirements.v2.txt b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/requirements.v2.txt new file mode 100644 index 000000000..940083b7f --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/requirements.v2.txt @@ -0,0 +1,31 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +agentscope>=2.0.0,<3.0.0 +aiohttp<3.9; python_version < "3.12" +pytest +pytest-asyncio +pytest-cov +pytest-vcr>=1.0.2 +vcrpy>=8.1.1 +pyyaml>=6.0 +packaging>=18.0 +opentelemetry-api>=1.39.0 +opentelemetry-sdk>=1.39.0 +opentelemetry-instrumentation>=0.60b0 +opentelemetry-semantic-conventions>=0.60b0 +wrapt>=1.17.3,<2.0.0 + +-e instrumentation-loongsuite/loongsuite-instrumentation-agentscope +-e util/opentelemetry-util-genai diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/test_v2_instrumentation.py b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/test_v2_instrumentation.py new file mode 100644 index 000000000..9b592bc40 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/test_v2_instrumentation.py @@ -0,0 +1,305 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""AgentScope v2 instrumentation tests.""" + +from __future__ import annotations + +import asyncio +import importlib.metadata +import os +from types import SimpleNamespace + +import pytest + +agentscope = pytest.importorskip("agentscope") +if not importlib.metadata.version("agentscope").startswith("2."): + pytest.skip("AgentScope v2 tests require agentscope>=2,<3", allow_module_level=True) + +from agentscope.agent import Agent # noqa: E402 +from agentscope.credential import DashScopeCredential # noqa: E402 +from agentscope.message import TextBlock, UserMsg # noqa: E402 +from agentscope.model import ChatResponse, DashScopeChatModel # noqa: E402 +from agentscope.tool import ToolResponse # noqa: E402 + +from opentelemetry.instrumentation.agentscope._v2_middleware import ( # noqa: E402 + AgentScopeV2Middleware, +) +from opentelemetry.instrumentation.agentscope.package import ( # noqa: E402 + get_installed_instrumentation_dependencies, +) +from opentelemetry.trace.status import StatusCode # noqa: E402 + + +def test_v2_dependency_detection(): + assert get_installed_instrumentation_dependencies() == ( + "agentscope >= 2.0.0, < 3.0.0", + ) + + +def test_instrumentor_injects_v2_middleware(instrument): + model = _make_model(stream=False) + agent = Agent( + name="middleware_probe", + system_prompt="Reply briefly.", + model=model, + ) + + assert any( + isinstance(middleware, AgentScopeV2Middleware) + for middleware in agent._reply_middlewares + ) + assert any( + isinstance(middleware, AgentScopeV2Middleware) + for middleware in agent._model_call_middlewares + ) + assert any( + isinstance(middleware, AgentScopeV2Middleware) + for middleware in agent._acting_middlewares + ) + + +def test_v2_uninstrument_removes_agent_patch(instrument): + instrument.uninstrument() + + agent = Agent( + name="uninstrument_probe", + system_prompt="Reply briefly.", + model=_make_model(stream=False), + ) + + assert not any( + isinstance(middleware, AgentScopeV2Middleware) + for middleware in agent._reply_middlewares + ) + assert not any( + isinstance(middleware, AgentScopeV2Middleware) + for middleware in agent._model_call_middlewares + ) + + +async def test_v2_existing_agent_middleware_noops_after_uninstrument( + instrument, span_exporter +): + agent = Agent( + name="existing_agent", + system_prompt="Reply briefly.", + model=_make_model(stream=False), + ) + middleware = _middleware(agent._model_call_middlewares) + instrument.uninstrument() + + async def model_handler(**kwargs): + del kwargs + return ChatResponse(content=[TextBlock(text="ok")], is_last=True) + + response = await middleware.on_model_call( + agent, + { + "current_model": agent.model, + "messages": [UserMsg(name="user", content="hello")], + }, + model_handler, + ) + + assert response.content + assert not span_exporter.get_finished_spans() + + +async def test_v2_model_call_error_path(instrument, span_exporter): + agent = Agent( + name="error_agent", + system_prompt="Reply briefly.", + model=_make_model(stream=False), + ) + middleware = _middleware(agent._model_call_middlewares) + + async def failing_handler(**kwargs): + del kwargs + raise RuntimeError("model failed") + + with pytest.raises(RuntimeError, match="model failed"): + await middleware.on_model_call( + agent, + { + "current_model": agent.model, + "messages": [UserMsg(name="user", content="fail")], + }, + failing_handler, + ) + + span = _spans_by_operation(span_exporter.get_finished_spans(), "chat")[0] + assert span.status.status_code == StatusCode.ERROR + assert span.attributes["error.type"] == "RuntimeError" + + +async def test_v2_streaming_model_call_error_path(instrument, span_exporter): + agent = Agent( + name="stream_error_agent", + system_prompt="Reply briefly.", + model=_make_model(stream=True), + ) + middleware = _middleware(agent._model_call_middlewares) + + async def failing_stream(): + yield ChatResponse(content=[TextBlock(text="partial")], is_last=False) + raise RuntimeError("stream failed") + + async def stream_handler(**kwargs): + del kwargs + return failing_stream() + + stream = await middleware.on_model_call( + agent, + { + "current_model": agent.model, + "messages": [UserMsg(name="user", content="fail")], + }, + stream_handler, + ) + + with pytest.raises(RuntimeError, match="stream failed"): + async for _ in stream: + pass + + span = _spans_by_operation(span_exporter.get_finished_spans(), "chat")[0] + assert span.status.status_code == StatusCode.ERROR + assert span.attributes["error.type"] == "RuntimeError" + + +async def test_v2_tool_acting_hook(instrument, span_exporter): + agent = Agent( + name="tool_agent", + system_prompt="Use tools.", + model=_make_model(stream=False), + ) + middleware = _middleware(agent._acting_middlewares) + tool_call = SimpleNamespace( + name="lookup_weather", + id="tool-call-1", + input='{"city": "Hangzhou"}', + ) + + async def tool_handler(**kwargs): + del kwargs + yield ToolResponse(content=[TextBlock(text="sunny")]) + + results = [ + item + async for item in middleware.on_acting( + agent, + {"tool_call": tool_call}, + tool_handler, + ) + ] + + assert results + tool_span = _spans_by_operation( + span_exporter.get_finished_spans(), "execute_tool" + )[0] + assert tool_span.attributes["gen_ai.tool.name"] == "lookup_weather" + + +@pytest.mark.vcr() +async def test_v2_agent_non_streaming_e2e(instrument, span_exporter): + model = _make_model(stream=False) + agent = Agent( + name="non_stream_agent", + system_prompt="Reply with exactly: OK", + model=model, + ) + + msg = await agent.reply(UserMsg(name="user", content="Say OK.")) + + assert msg.get_text_content() + _assert_agent_and_llm_spans(span_exporter.get_finished_spans()) + + +@pytest.mark.vcr() +async def test_v2_agent_streaming_e2e(instrument, span_exporter): + model = _make_model(stream=True) + agent = Agent( + name="stream_agent", + system_prompt="Reply with a short sentence.", + model=model, + ) + + events = [ + event + async for event in agent.reply_stream( + UserMsg(name="user", content="Say hello in one sentence.") + ) + ] + + assert events + assert any(event.__class__.__name__ == "TextBlockDeltaEvent" for event in events) + _assert_agent_and_llm_spans(span_exporter.get_finished_spans()) + + +@pytest.mark.vcr() +async def test_v2_agent_concurrent_e2e(instrument, span_exporter): + async def call_agent(idx: int): + agent = Agent( + name=f"concurrent_agent_{idx}", + system_prompt="Reply with exactly one short sentence.", + model=_make_model(stream=False), + ) + return await agent.reply( + UserMsg(name="user", content=f"Say OK for request {idx}.") + ) + + results = await asyncio.gather(call_agent(1), call_agent(2)) + + assert all(result.get_text_content() for result in results) + spans = span_exporter.get_finished_spans() + agent_spans = _spans_by_operation(spans, "invoke_agent") + llm_spans = _spans_by_operation(spans, "chat") + assert len(agent_spans) == 2 + assert len(llm_spans) == 2 + agent_span_ids = {span.context.span_id for span in agent_spans} + assert {span.parent.span_id for span in llm_spans} == agent_span_ids + + +def _make_model(stream: bool): + return DashScopeChatModel( + credential=DashScopeCredential(api_key=os.environ["DASHSCOPE_API_KEY"]), + model="qwen-plus", + parameters=DashScopeChatModel.Parameters( + max_tokens=16, + thinking_enable=False, + ), + stream=stream, + max_retries=0, + ) + + +def _assert_agent_and_llm_spans(spans): + assert _spans_by_operation(spans, "invoke_agent") + assert _spans_by_operation(spans, "chat") + + +def _spans_by_operation(spans, operation_name): + return [ + span + for span in spans + if span.attributes.get("gen_ai.operation.name") == operation_name + ] + + +def _middleware(middlewares): + return next( + middleware + for middleware in middlewares + if isinstance(middleware, AgentScopeV2Middleware) + ) diff --git a/tox-loongsuite.ini b/tox-loongsuite.ini index 395e7fccc..fa5fb3440 100644 --- a/tox-loongsuite.ini +++ b/tox-loongsuite.ini @@ -14,6 +14,7 @@ envlist = ; loongsuite-instrumentation-agentscope py3{10,11,12,13}-test-loongsuite-instrumentation-agentscope-{oldest,latest} + py3{11,12,13}-test-loongsuite-instrumentation-agentscopev2 lint-loongsuite-instrumentation-agentscope ; loongsuite-instrumentation-dashscope @@ -152,6 +153,7 @@ deps = agentscope-oldest: -r {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/requirements.oldest.txt agentscope-latest: {[testenv]test_deps} agentscope-latest: -r {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/requirements.latest.txt + agentscopev2: -r {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/requirements.v2.txt lint-loongsuite-instrumentation-agentscope: -r {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/requirements.oldest.txt dashscope-oldest: -r {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/tests/requirements.oldest.txt @@ -264,6 +266,9 @@ allowlist_externals = sh pytest +passenv = + DASHSCOPE_API_KEY + setenv = ; override CORE_REPO_SHA via env variable when testing other branches/commits ; than the default pinned 0.62-compatible core commit @@ -279,6 +284,7 @@ commands_pre = commands = test-loongsuite-instrumentation-agentscope: pytest {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests {posargs} + test-loongsuite-instrumentation-agentscopev2: pytest {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/test_v2_instrumentation.py {posargs} lint-loongsuite-instrumentation-agentscope: python -m ruff check {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-agentscope test-loongsuite-instrumentation-dashscope: pytest {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/tests {posargs} From 9da86559ed21ba627b8bdd9996a90fed2fd1adbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=81=E5=B1=BF?= Date: Wed, 3 Jun 2026 16:30:11 +0800 Subject: [PATCH 16/21] Support AgentScope v2 instrumentation --- instrumentation-loongsuite/README.md | 2 +- .../agentscope/_v2_middleware.py | 72 ++++++++++-- .../tests/conftest.py | 73 ++++++++++++ .../tests/requirements.latest.txt | 25 ++-- .../tests/requirements.v2.txt | 31 ----- .../tests/test_v2_instrumentation.py | 107 +++++++++++++++++- tox-loongsuite.ini | 10 +- 7 files changed, 249 insertions(+), 71 deletions(-) delete mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/requirements.v2.txt diff --git a/instrumentation-loongsuite/README.md b/instrumentation-loongsuite/README.md index 325f02740..8c6732cee 100644 --- a/instrumentation-loongsuite/README.md +++ b/instrumentation-loongsuite/README.md @@ -1,7 +1,7 @@ | Instrumentation | Supported Packages | Metrics support | Semconv status | | --------------- | ------------------ | --------------- | -------------- | -| [loongsuite-instrumentation-agentscope](./loongsuite-instrumentation-agentscope) | agentscope >= 1.0.0 | No | development +| [loongsuite-instrumentation-agentscope](./loongsuite-instrumentation-agentscope) | agentscope >= 1.0.0, < 3.0.0 | No | development | [loongsuite-instrumentation-agno](./loongsuite-instrumentation-agno) | agno >= 2.0.0, < 3 | No | development | [loongsuite-instrumentation-algotune](./loongsuite-instrumentation-algotune) | algotune | No | development | [loongsuite-instrumentation-bfclv4](./loongsuite-instrumentation-bfclv4) | bfcl-eval >= 4.0.0 | No | development diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/_v2_middleware.py b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/_v2_middleware.py index 2920490d3..f70d06d8f 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/_v2_middleware.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/_v2_middleware.py @@ -21,6 +21,8 @@ import logging import timeit from collections.abc import AsyncGenerator, Awaitable, Callable, Sequence +from contextvars import ContextVar +from dataclasses import asdict, is_dataclass from typing import Any from agentscope.agent import Agent @@ -34,6 +36,7 @@ from opentelemetry.util.genai.extended_types import ( ExecuteToolInvocation, InvokeAgentInvocation, + ReactStepInvocation, ) from opentelemetry.util.genai.types import ( Error, @@ -102,6 +105,10 @@ def __init__( self, handler: Callable[[], ExtendedTelemetryHandler | None] ) -> None: self._handler = handler + self._react_round: ContextVar[int] = ContextVar( + "loongsuite_agentscope_v2_react_round", + default=0, + ) async def on_reply( self, @@ -117,6 +124,7 @@ async def on_reply( invocation = _create_agent_invocation(agent, input_kwargs) handler.start_invoke_agent(invocation) + round_token = self._react_round.set(0) first_token_seen = False last_msg = None closed = False @@ -144,6 +152,7 @@ async def on_reply( handler.stop_invoke_agent(invocation) closed = True finally: + self._react_round.reset(round_token) if not closed: handler.stop_invoke_agent(invocation) @@ -246,6 +255,8 @@ async def on_acting( return tool_call = input_kwargs.get("tool_call") + react_invocation = ReactStepInvocation(round=self._next_react_round()) + handler.start_react_step(react_invocation, context=get_current()) invocation = ExecuteToolInvocation( tool_name=getattr(tool_call, "name", "unknown_tool"), tool_call_id=getattr(tool_call, "id", None), @@ -254,28 +265,46 @@ async def on_acting( ) handler.start_execute_tool(invocation) last_item = None - closed = False + tool_closed = False + react_closed = False try: async for item in next_handler(**input_kwargs): last_item = item yield item except BaseException as exc: + error = Error( + message=str(exc) or type(exc).__name__, type=type(exc) + ) handler.fail_execute_tool( invocation, - Error(message=str(exc) or type(exc).__name__, type=type(exc)), + error, ) - closed = True + tool_closed = True + handler.fail_react_step(react_invocation, error) + react_closed = True raise else: if isinstance(last_item, ToolResponse): - invocation.tool_call_result = last_item.content + invocation.tool_call_result = _jsonable( + _blocks_to_parts(last_item.content) + ) elif last_item is not None: invocation.tool_call_result = str(last_item) handler.stop_execute_tool(invocation) - closed = True + tool_closed = True + react_invocation.finish_reason = "tool_calls" + handler.stop_react_step(react_invocation) + react_closed = True finally: - if not closed: + if not tool_closed: handler.stop_execute_tool(invocation) + if not react_closed: + handler.stop_react_step(react_invocation) + + def _next_react_round(self) -> int: + current = self._react_round.get() + 1 + self._react_round.set(current) + return current def _create_agent_invocation( @@ -290,10 +319,14 @@ def _create_agent_invocation( provider=provider, agent_name=getattr(agent, "name", "unknown_agent"), agent_id=getattr(getattr(agent, "state", None), "session_id", None), - conversation_id=getattr(getattr(agent, "state", None), "session_id", None), + conversation_id=getattr( + getattr(agent, "state", None), "session_id", None + ), request_model=request_model, input_messages=_messages_to_inputs(inputs), - system_instruction=[Text(content=getattr(agent, "_system_prompt", ""))], + system_instruction=[ + Text(content=getattr(agent, "_system_prompt", "")) + ], ) @@ -335,7 +368,9 @@ def _messages_to_inputs(value: Any) -> list[InputMessage]: if isinstance(value, Msg): return [_message_to_input(value)] if isinstance(value, list): - return [_message_to_input(item) for item in value if isinstance(item, Msg)] + return [ + _message_to_input(item) for item in value if isinstance(item, Msg) + ] return [] @@ -353,7 +388,10 @@ def _message_to_output(msg: Msg) -> OutputMessage: def _chat_response_to_output(response: ChatResponse) -> OutputMessage: finish_reason = "stop" - if any(getattr(block, "type", None) == "tool_call" for block in response.content): + if any( + getattr(block, "type", None) == "tool_call" + for block in response.content + ): finish_reason = "tool_calls" return OutputMessage( role="assistant", @@ -434,7 +472,9 @@ def _middleware_arg_position() -> int | None: return None -def _is_streaming_model(model: ChatModelBase, input_kwargs: dict[str, Any]) -> bool: +def _is_streaming_model( + model: ChatModelBase, input_kwargs: dict[str, Any] +) -> bool: if "stream" in input_kwargs: return bool(input_kwargs["stream"]) return bool(getattr(model, "stream", False)) @@ -449,6 +489,16 @@ def _loads_json(value: Any) -> Any: return value +def _jsonable(value: Any) -> Any: + if is_dataclass(value): + return _jsonable(asdict(value)) + if isinstance(value, list | tuple): + return [_jsonable(item) for item in value] + if isinstance(value, dict): + return {key: _jsonable(item) for key, item in value.items()} + return value + + def _set_if_present( invocation: LLMInvocation, field_name: str, diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/conftest.py b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/conftest.py index bd2976159..2e94d564e 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/conftest.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/conftest.py @@ -15,8 +15,12 @@ # -*- coding: utf-8 -*- """Test Configuration""" +import asyncio +import inspect import json import os +from importlib.metadata import PackageNotFoundError, version +from pathlib import Path import pytest import yaml @@ -27,6 +31,49 @@ if "DASHSCOPE_API_KEY" not in os.environ: os.environ["DASHSCOPE_API_KEY"] = "test_api_key" +# vcrpy's aiohttp stub still references a mixin removed by newer aiohttp +# releases. AgentScope tests use VCR for replay and should not fail during +# pytest marker setup just because aiohttp is importable in the environment. +try: + import aiohttp.streams # type: ignore[import-not-found] + + if not hasattr(aiohttp.streams, "AsyncStreamReaderMixin"): + aiohttp.streams.AsyncStreamReaderMixin = object +except ImportError: + pass + +try: + import aiohttp # type: ignore[import-not-found] + import vcr.stubs.aiohttp_stubs as aiohttp_stubs + + if ( + "stream_writer" + in inspect.signature(aiohttp.ClientResponse.__init__).parameters + ): + + class _CompatStreamWriter: + output_size = 0 + + class _CompatMockClientResponse(aiohttp_stubs.MockClientResponse): + def __init__(self, method, url, request_info=None): + aiohttp.ClientResponse.__init__( + self, + method=method, + url=url, + writer=None, + continue100=None, + timer=None, + request_info=request_info, + traces=None, + loop=asyncio.get_event_loop(), + session=None, + stream_writer=_CompatStreamWriter(), + ) + + aiohttp_stubs.MockClientResponse = _CompatMockClientResponse +except ImportError: + pass + from opentelemetry.instrumentation.agentscope import AgentScopeInstrumentor from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk._logs.export import ( @@ -45,6 +92,19 @@ OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, ) +_V2_TEST_FILE = "test_v2_instrumentation.py" + + +def _agentscope_major() -> int: + try: + installed_version = version("agentscope") + except PackageNotFoundError: + return 1 + try: + return int(installed_version.split(".", 1)[0]) + except ValueError: + return 1 + def pytest_configure(config: pytest.Config): # Configure pytest-asyncio to auto-detect async test functions @@ -67,6 +127,19 @@ def pytest_configure(config: pytest.Config): config.option.api_key = api_key +def pytest_ignore_collect(collection_path, config): # noqa: ARG001 + path = Path(str(collection_path)) + if not path.name.startswith("test_") or path.suffix != ".py": + return None + + major = _agentscope_major() + if major >= 2: + return path.name != _V2_TEST_FILE + if path.name == _V2_TEST_FILE: + return True + return None + + # ==================== Exporters and Readers ==================== diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/requirements.latest.txt b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/requirements.latest.txt index d3cf39b4f..df0dc4ef6 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/requirements.latest.txt +++ b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/requirements.latest.txt @@ -16,25 +16,14 @@ # WARNING: NOT HERMETIC !!!!!!!!!! # ******************************** # -# This "requirements.txt" is installed in conjunction -# with multiple other dependencies in the top-level "tox-loongsuite.ini" -# file. In particular, please see: -# -# agentscope-latest: {[testenv]test_deps} -# agentscope-latest: -r {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/requirements.latest.txt -# -# This provides additional dependencies, namely: -# -# opentelemetry-api -# opentelemetry-sdk -# opentelemetry-semantic-conventions -# -# ... with a "dev" version based on the latest distribution. +# This file intentionally uses stable OpenTelemetry dependencies instead of +# the shared core-dev test_deps. AgentScope v2 depends on stable OTel exporter +# packages, which conflict with opentelemetry-test-utils from the core repo. # This variant of the requirements aims to test the system using # the newest supported version of external dependencies. -agentscope>=1.0.0,<2.0.0 +agentscope>=2.0.0,<3.0.0 pytest pytest-asyncio pytest-cov @@ -43,9 +32,11 @@ vcrpy>=8.1.1 aiohttp<3.12 pyyaml>=6.0 packaging>=18.0 -aiohttp<3.9; python_version < "3.12" +opentelemetry-api>=1.39.0 +opentelemetry-sdk>=1.39.0 +opentelemetry-instrumentation>=0.60b0 +opentelemetry-semantic-conventions>=0.60b0 wrapt>=1.17.3,<2.0.0 --e opentelemetry-instrumentation -e instrumentation-loongsuite/loongsuite-instrumentation-agentscope -e util/opentelemetry-util-genai diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/requirements.v2.txt b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/requirements.v2.txt deleted file mode 100644 index 940083b7f..000000000 --- a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/requirements.v2.txt +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright The OpenTelemetry Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -agentscope>=2.0.0,<3.0.0 -aiohttp<3.9; python_version < "3.12" -pytest -pytest-asyncio -pytest-cov -pytest-vcr>=1.0.2 -vcrpy>=8.1.1 -pyyaml>=6.0 -packaging>=18.0 -opentelemetry-api>=1.39.0 -opentelemetry-sdk>=1.39.0 -opentelemetry-instrumentation>=0.60b0 -opentelemetry-semantic-conventions>=0.60b0 -wrapt>=1.17.3,<2.0.0 - --e instrumentation-loongsuite/loongsuite-instrumentation-agentscope --e util/opentelemetry-util-genai diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/test_v2_instrumentation.py b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/test_v2_instrumentation.py index 9b592bc40..ab2254a55 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/test_v2_instrumentation.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/test_v2_instrumentation.py @@ -25,7 +25,9 @@ agentscope = pytest.importorskip("agentscope") if not importlib.metadata.version("agentscope").startswith("2."): - pytest.skip("AgentScope v2 tests require agentscope>=2,<3", allow_module_level=True) + pytest.skip( + "AgentScope v2 tests require agentscope>=2,<3", allow_module_level=True + ) from agentscope.agent import Agent # noqa: E402 from agentscope.credential import DashScopeCredential # noqa: E402 @@ -211,6 +213,101 @@ async def tool_handler(**kwargs): assert tool_span.attributes["gen_ai.tool.name"] == "lookup_weather" +async def test_v2_tool_result_content_capture( + instrument_with_content, + span_exporter, +): + agent = Agent( + name="tool_content_agent", + system_prompt="Use tools.", + model=_make_model(stream=False), + ) + middleware = _middleware(agent._acting_middlewares) + tool_call = SimpleNamespace( + name="lookup_weather", + id="tool-call-content", + input='{"city": "Hangzhou"}', + ) + + async def tool_handler(**kwargs): + del kwargs + yield ToolResponse(content=[TextBlock(text="sunny")]) + + results = [ + item + async for item in middleware.on_acting( + agent, + {"tool_call": tool_call}, + tool_handler, + ) + ] + + assert results + tool_span = _spans_by_operation( + span_exporter.get_finished_spans(), "execute_tool" + )[0] + assert tool_span.attributes["gen_ai.tool.call.result"] == ( + '[{"content":"sunny","type":"text"}]' + ) + + +async def test_v2_react_many_tools_telemetry(instrument, span_exporter): + agent = Agent( + name="react_tool_agent", + system_prompt="Use tools.", + model=_make_model(stream=False), + ) + middleware = _middleware(agent._acting_middlewares) + + for idx, name in enumerate( + [ + "lookup_weather", + "search_docs", + "calculate_total", + "write_summary", + ], + start=1, + ): + tool_call = SimpleNamespace( + name=name, + id=f"tool-call-{idx}", + input=f'{{"idx": {idx}}}', + ) + + async def tool_handler(**kwargs): + del kwargs + yield ToolResponse(content=[TextBlock(text=f"result {idx}")]) + + results = [ + item + async for item in middleware.on_acting( + agent, + {"tool_call": tool_call}, + tool_handler, + ) + ] + assert results + + spans = span_exporter.get_finished_spans() + react_spans = _spans_by_operation(spans, "react") + tool_spans = _spans_by_operation(spans, "execute_tool") + + assert [span.attributes["gen_ai.react.round"] for span in react_spans] == [ + 1, + 2, + 3, + 4, + ] + assert {span.attributes["gen_ai.tool.name"] for span in tool_spans} == { + "lookup_weather", + "search_docs", + "calculate_total", + "write_summary", + } + react_span_ids = {span.context.span_id for span in react_spans} + assert {span.parent.span_id for span in tool_spans} == react_span_ids + + @pytest.mark.vcr() async def test_v2_agent_non_streaming_e2e(instrument, span_exporter): model = _make_model(stream=False) @@ -243,7 +340,9 @@ async def test_v2_agent_streaming_e2e(instrument, span_exporter): ] assert events - assert any(event.__class__.__name__ == "TextBlockDeltaEvent" for event in events) + assert any( + event.__class__.__name__ == "TextBlockDeltaEvent" for event in events + ) _assert_agent_and_llm_spans(span_exporter.get_finished_spans()) @@ -273,7 +372,9 @@ async def call_agent(idx: int): def _make_model(stream: bool): return DashScopeChatModel( - credential=DashScopeCredential(api_key=os.environ["DASHSCOPE_API_KEY"]), + credential=DashScopeCredential( + api_key=os.environ["DASHSCOPE_API_KEY"] + ), model="qwen-plus", parameters=DashScopeChatModel.Parameters( max_tokens=16, diff --git a/tox-loongsuite.ini b/tox-loongsuite.ini index fa5fb3440..15d2d3ff3 100644 --- a/tox-loongsuite.ini +++ b/tox-loongsuite.ini @@ -13,8 +13,8 @@ envlist = ; FIXME(cirilla-zmh): refactor test of original instrumentation module ; loongsuite-instrumentation-agentscope - py3{10,11,12,13}-test-loongsuite-instrumentation-agentscope-{oldest,latest} - py3{11,12,13}-test-loongsuite-instrumentation-agentscopev2 + py3{10,11,12,13}-test-loongsuite-instrumentation-agentscope-oldest + py3{11,12,13}-test-loongsuite-instrumentation-agentscope-latest lint-loongsuite-instrumentation-agentscope ; loongsuite-instrumentation-dashscope @@ -151,9 +151,7 @@ deps = coverage: pytest-cov agentscope-oldest: -r {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/requirements.oldest.txt - agentscope-latest: {[testenv]test_deps} agentscope-latest: -r {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/requirements.latest.txt - agentscopev2: -r {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/requirements.v2.txt lint-loongsuite-instrumentation-agentscope: -r {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/requirements.oldest.txt dashscope-oldest: -r {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/tests/requirements.oldest.txt @@ -266,9 +264,6 @@ allowlist_externals = sh pytest -passenv = - DASHSCOPE_API_KEY - setenv = ; override CORE_REPO_SHA via env variable when testing other branches/commits ; than the default pinned 0.62-compatible core commit @@ -284,7 +279,6 @@ commands_pre = commands = test-loongsuite-instrumentation-agentscope: pytest {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests {posargs} - test-loongsuite-instrumentation-agentscopev2: pytest {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/test_v2_instrumentation.py {posargs} lint-loongsuite-instrumentation-agentscope: python -m ruff check {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-agentscope test-loongsuite-instrumentation-dashscope: pytest {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-dashscope/tests {posargs} From 7976aa125e83a16e6f98d668a6306af18b76b73f Mon Sep 17 00:00:00 2001 From: sipercai <53717475+sipercai@users.noreply.github.com> Date: Thu, 25 Jun 2026 11:46:29 +0800 Subject: [PATCH 17/21] fix(agentscope): serialize v2 tool result content --- .../agentscope/_v2_middleware.py | 10 +++++- .../tests/test_v2_instrumentation.py | 31 ++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/_v2_middleware.py b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/_v2_middleware.py index f70d06d8f..855813503 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/_v2_middleware.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/_v2_middleware.py @@ -420,12 +420,20 @@ def _blocks_to_parts(blocks: Sequence[Any]) -> list[Any]: parts.append( ToolCallResponse( id=getattr(block, "id", None), - response=getattr(block, "output", ""), + response=_tool_result_response( + getattr(block, "output", "") + ), ) ) return parts +def _tool_result_response(value: Any) -> Any: + if isinstance(value, list | tuple): + return _blocks_to_parts(value) + return value + + def _tool_definitions(tools: list[dict[str, Any]] | None) -> list[Any]: if not tools: return [] diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/test_v2_instrumentation.py b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/test_v2_instrumentation.py index ab2254a55..336d4ac8a 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/test_v2_instrumentation.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/tests/test_v2_instrumentation.py @@ -19,6 +19,7 @@ import asyncio import importlib.metadata import os +from dataclasses import asdict from types import SimpleNamespace import pytest @@ -31,17 +32,24 @@ from agentscope.agent import Agent # noqa: E402 from agentscope.credential import DashScopeCredential # noqa: E402 -from agentscope.message import TextBlock, UserMsg # noqa: E402 +from agentscope.message import ( # noqa: E402 + Msg, + TextBlock, + ToolResultBlock, + UserMsg, +) from agentscope.model import ChatResponse, DashScopeChatModel # noqa: E402 from agentscope.tool import ToolResponse # noqa: E402 from opentelemetry.instrumentation.agentscope._v2_middleware import ( # noqa: E402 AgentScopeV2Middleware, + _message_to_input, ) from opentelemetry.instrumentation.agentscope.package import ( # noqa: E402 get_installed_instrumentation_dependencies, ) from opentelemetry.trace.status import StatusCode # noqa: E402 +from opentelemetry.util.genai.utils import gen_ai_json_dumps # noqa: E402 def test_v2_dependency_detection(): @@ -50,6 +58,27 @@ def test_v2_dependency_detection(): ) +def test_v2_tool_result_message_content_is_jsonable(): + msg = Msg( + name="assistant", + role="assistant", + content=[ + ToolResultBlock( + id="tool-call-content", + name="lookup_weather", + output=[TextBlock(text="sunny")], + ) + ], + ) + + input_message = _message_to_input(msg) + + assert ( + gen_ai_json_dumps([asdict(input_message)]) + == '[{"role":"assistant","parts":[{"response":[{"content":"sunny","type":"text"}],"id":"tool-call-content","type":"tool_call_response"}]}]' + ) + + def test_instrumentor_injects_v2_middleware(instrument): model = _make_model(stream=False) agent = Agent( From b940fa6dd448d282cf5a6ffccf649162532abc44 Mon Sep 17 00:00:00 2001 From: sipercai <53717475+sipercai@users.noreply.github.com> Date: Thu, 25 Jun 2026 14:37:33 +0800 Subject: [PATCH 18/21] docs(agentscope): clarify v2 handler factory --- .../src/opentelemetry/instrumentation/agentscope/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/__init__.py b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/__init__.py index 2002bd3f9..f3b7373ba 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/__init__.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-agentscope/src/opentelemetry/instrumentation/agentscope/__init__.py @@ -356,6 +356,8 @@ def wrap_agent_init(wrapped, instance, args, kwargs): args, kwargs = append_loongsuite_middleware( args, kwargs, + # Resolve the handler lazily so reinstrumentation uses the + # current handler instead of the one captured at init time. AgentScopeV2Middleware(handler=lambda: self._handler), ) return wrapped(*args, **kwargs) From 02c0c31675e896ece7360f609b8a234c9086e365 Mon Sep 17 00:00:00 2001 From: sipercai <53717475+sipercai@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:33:35 +0800 Subject: [PATCH 19/21] chore: clean integration whitespace --- .../tests/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/__init__.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/__init__.py index f87ce79b7..b0a6f4284 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/__init__.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/__init__.py @@ -11,4 +11,3 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - From c77e2f300a1a14c5af09da745ef86e8907c7b564 Mon Sep 17 00:00:00 2001 From: sipercai <53717475+sipercai@users.noreply.github.com> Date: Wed, 1 Jul 2026 17:40:42 +0800 Subject: [PATCH 20/21] fix: set MAF framework provider for agent executor spans --- .../span_processor.py | 11 ++++++++++ .../util_genai_bridge.py | 6 +++++ .../tests/test_processor.py | 3 +++ .../tests/test_util_genai_bridge.py | 22 +++++++++++++++++++ 4 files changed, 42 insertions(+) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py index b9a556d77..8adab3b0d 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/span_processor.py @@ -94,6 +94,7 @@ _EDGE_GROUP_PROCESS = "edge_group.process" _LIVE_SPAN_MAX_AGE_NS = 60 * 1_000_000_000 _GEN_AI_TOOL_NAME = "gen_ai.tool.name" +_FRAMEWORK_PROVIDER_NAME = "microsoft.agent_framework" def _attr_value(span: Any, key: str) -> Any: @@ -876,6 +877,16 @@ def _apply_semantic_attributes( normalized = _normalize_provider(provider) if normalized is not None and normalized != provider: _set_attr(live, GEN_AI_PROVIDER_NAME, normalized) + elif ( + normalized is None + and span_kind == GenAISpanKind.AGENT + and op_name + in { + GenAIOperation.CREATE_AGENT, + GenAIOperation.INVOKE_AGENT, + } + ): + _set_attr(live, GEN_AI_PROVIDER_NAME, _FRAMEWORK_PROVIDER_NAME) # 5) Normalize finish reasons written by MAF as a JSON string. _normalize_finish_reasons(live, readable) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/util_genai_bridge.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/util_genai_bridge.py index e152501dd..b4257182d 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/util_genai_bridge.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/src/opentelemetry/instrumentation/microsoft_agent_framework/util_genai_bridge.py @@ -76,6 +76,7 @@ GenAISpanKind, ) from .span_processor import ( + _FRAMEWORK_PROVIDER_NAME, _attr_value, _classify_span, _is_exception_event, @@ -598,6 +599,11 @@ def _set_common_live_attributes( provider = _normalize_provider(_attr_value(span, GEN_AI_PROVIDER_NAME)) if provider is not None: span.set_attribute(GEN_AI_PROVIDER_NAME, provider) + elif span_kind == GenAISpanKind.AGENT and op_name in { + GenAIOperation.CREATE_AGENT, + GenAIOperation.INVOKE_AGENT, + }: + span.set_attribute(GEN_AI_PROVIDER_NAME, _FRAMEWORK_PROVIDER_NAME) finish_reasons = _finish_reasons(span) if finish_reasons: span.set_attribute(GEN_AI_RESPONSE_FINISH_REASONS, finish_reasons) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_processor.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_processor.py index ffa7c4dcd..265374f74 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_processor.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_processor.py @@ -207,6 +207,9 @@ def test_executor_process_agent_executor_becomes_agent(): assert ( s.attributes.get(GEN_AI_OPERATION_NAME) == GenAIOperation.INVOKE_AGENT ) + assert ( + s.attributes.get(GEN_AI_PROVIDER_NAME) == "microsoft.agent_framework" + ) def test_executor_process_unknown_executor_stays_workflow_operation(): diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_util_genai_bridge.py b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_util_genai_bridge.py index 84916916e..c40e4cf5b 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_util_genai_bridge.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/test_util_genai_bridge.py @@ -303,6 +303,28 @@ def test_agent_span_is_finalized_by_util_genai_before_export(monkeypatch): assert span.attributes.get("gen_ai.agent.id") == "agent-1" +def test_agent_span_gets_framework_provider_when_missing(monkeypatch): + obs_mod, exporter = _install_fake_observability(monkeypatch) + util_genai_bridge.apply_util_genai_bridge() + try: + with obs_mod._get_span( + { + GEN_AI_OPERATION_NAME: GenAIOperation.INVOKE_AGENT, + "gen_ai.agent.name": "planner", + }, + "gen_ai.agent.name", + ): + pass + finally: + util_genai_bridge.revert_util_genai_bridge() + + span = exporter.get_finished_spans()[0] + assert ( + span.attributes.get(GEN_AI_PROVIDER_NAME) + == "microsoft.agent_framework" + ) + + def test_mcp_span_is_seeded_before_export(monkeypatch): obs_mod, exporter = _install_fake_observability(monkeypatch) util_genai_bridge.apply_util_genai_bridge() From 3f634713310e71f1e606604303860aff1c49bbc0 Mon Sep 17 00:00:00 2001 From: sipercai <53717475+sipercai@users.noreply.github.com> Date: Fri, 3 Jul 2026 14:09:06 +0800 Subject: [PATCH 21/21] chore: refresh loongsuite generated files after rebase --- .github/workflows/loongsuite_test_0.yml | 2 +- instrumentation-loongsuite/README.md | 3 ++ .../loongsuite_instrumentation_agentscope.py | 2 +- ...strumentation_microsoft_agent_framework.py | 30 +++++++++++++++++++ 4 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_microsoft_agent_framework.py diff --git a/.github/workflows/loongsuite_test_0.yml b/.github/workflows/loongsuite_test_0.yml index b3b3d1598..6f26f21fa 100644 --- a/.github/workflows/loongsuite_test_0.yml +++ b/.github/workflows/loongsuite_test_0.yml @@ -84,7 +84,7 @@ jobs: shell: bash env: LOONGSUITE_ALL_JOBS: >- - [{"name": "py310-test-loongsuite-instrumentation-agentscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-agentscope-oldest", "ui_name": "loongsuite-instrumentation-agentscope-oldest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-agentscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-agentscope-latest", "ui_name": "loongsuite-instrumentation-agentscope-latest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-agentscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-agentscope-oldest", "ui_name": "loongsuite-instrumentation-agentscope-oldest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-agentscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-agentscope-latest", "ui_name": "loongsuite-instrumentation-agentscope-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-agentscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-agentscope-oldest", "ui_name": "loongsuite-instrumentation-agentscope-oldest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-agentscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-agentscope-latest", "ui_name": "loongsuite-instrumentation-agentscope-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-agentscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-agentscope-oldest", "ui_name": "loongsuite-instrumentation-agentscope-oldest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-agentscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-agentscope-latest", "ui_name": "loongsuite-instrumentation-agentscope-latest 3.13 ARC"}, {"name": "py39-test-loongsuite-instrumentation-dashscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-dashscope-oldest", "ui_name": "loongsuite-instrumentation-dashscope-oldest 3.9 ARC"}, {"name": "py39-test-loongsuite-instrumentation-dashscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-dashscope-latest", "ui_name": "loongsuite-instrumentation-dashscope-latest 3.9 ARC"}, {"name": "py310-test-loongsuite-instrumentation-dashscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-dashscope-oldest", "ui_name": "loongsuite-instrumentation-dashscope-oldest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-dashscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-dashscope-latest", "ui_name": "loongsuite-instrumentation-dashscope-latest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-dashscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-dashscope-oldest", "ui_name": "loongsuite-instrumentation-dashscope-oldest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-dashscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-dashscope-latest", "ui_name": "loongsuite-instrumentation-dashscope-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-dashscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-dashscope-oldest", "ui_name": "loongsuite-instrumentation-dashscope-oldest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-dashscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-dashscope-latest", "ui_name": "loongsuite-instrumentation-dashscope-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-dashscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-dashscope-oldest", "ui_name": "loongsuite-instrumentation-dashscope-oldest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-dashscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-dashscope-latest", "ui_name": "loongsuite-instrumentation-dashscope-latest 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-claude-agent-sdk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-claude-agent-sdk-oldest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-oldest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-claude-agent-sdk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-claude-agent-sdk-latest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-latest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-claude-agent-sdk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-claude-agent-sdk-oldest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-oldest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-claude-agent-sdk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-claude-agent-sdk-latest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-claude-agent-sdk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-claude-agent-sdk-oldest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-oldest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-claude-agent-sdk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-claude-agent-sdk-latest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-claude-agent-sdk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-claude-agent-sdk-oldest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-oldest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-claude-agent-sdk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-claude-agent-sdk-latest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-latest 3.13 ARC"}, {"name": "py39-test-loongsuite-instrumentation-google-adk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-google-adk-oldest", "ui_name": "loongsuite-instrumentation-google-adk-oldest 3.9 ARC"}, {"name": "py39-test-loongsuite-instrumentation-google-adk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-google-adk-latest", "ui_name": "loongsuite-instrumentation-google-adk-latest 3.9 ARC"}, {"name": "py310-test-loongsuite-instrumentation-google-adk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-google-adk-oldest", "ui_name": "loongsuite-instrumentation-google-adk-oldest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-google-adk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-google-adk-latest", "ui_name": "loongsuite-instrumentation-google-adk-latest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-google-adk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-google-adk-oldest", "ui_name": "loongsuite-instrumentation-google-adk-oldest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-google-adk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-google-adk-latest", "ui_name": "loongsuite-instrumentation-google-adk-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-google-adk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-google-adk-oldest", "ui_name": "loongsuite-instrumentation-google-adk-oldest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-google-adk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-google-adk-latest", "ui_name": "loongsuite-instrumentation-google-adk-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-google-adk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-google-adk-oldest", "ui_name": "loongsuite-instrumentation-google-adk-oldest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-google-adk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-google-adk-latest", "ui_name": "loongsuite-instrumentation-google-adk-latest 3.13 ARC"}, {"name": "py39-test-loongsuite-instrumentation-agno_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agno", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-agno", "ui_name": "loongsuite-instrumentation-agno 3.9 ARC"}, {"name": "py310-test-loongsuite-instrumentation-agno_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agno", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-agno", "ui_name": "loongsuite-instrumentation-agno 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-agno_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agno", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-agno", "ui_name": "loongsuite-instrumentation-agno 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-agno_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agno", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-agno", "ui_name": "loongsuite-instrumentation-agno 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-agno_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agno", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-agno", "ui_name": "loongsuite-instrumentation-agno 3.13 ARC"}, {"name": "py39-test-loongsuite-instrumentation-langchain-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-langchain-oldest", "ui_name": "loongsuite-instrumentation-langchain-oldest 3.9 ARC"}, {"name": "py39-test-loongsuite-instrumentation-langchain-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-langchain-latest", "ui_name": "loongsuite-instrumentation-langchain-latest 3.9 ARC"}, {"name": "py310-test-loongsuite-instrumentation-langchain-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-langchain-oldest", "ui_name": "loongsuite-instrumentation-langchain-oldest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-langchain-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-langchain-latest", "ui_name": "loongsuite-instrumentation-langchain-latest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-langchain-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-langchain-oldest", "ui_name": "loongsuite-instrumentation-langchain-oldest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-langchain-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-langchain-latest", "ui_name": "loongsuite-instrumentation-langchain-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-langchain-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-langchain-oldest", "ui_name": "loongsuite-instrumentation-langchain-oldest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-langchain-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-langchain-latest", "ui_name": "loongsuite-instrumentation-langchain-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-langchain-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-langchain-oldest", "ui_name": "loongsuite-instrumentation-langchain-oldest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-langchain-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-langchain-latest", "ui_name": "loongsuite-instrumentation-langchain-latest 3.13 ARC"}, {"name": "py39-test-loongsuite-instrumentation-langgraph-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-langgraph-oldest", "ui_name": "loongsuite-instrumentation-langgraph-oldest 3.9 ARC"}, {"name": "py39-test-loongsuite-instrumentation-langgraph-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-langgraph-latest", "ui_name": "loongsuite-instrumentation-langgraph-latest 3.9 ARC"}, {"name": "py310-test-loongsuite-instrumentation-langgraph-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-langgraph-oldest", "ui_name": "loongsuite-instrumentation-langgraph-oldest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-langgraph-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-langgraph-latest", "ui_name": "loongsuite-instrumentation-langgraph-latest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-langgraph-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-langgraph-oldest", "ui_name": "loongsuite-instrumentation-langgraph-oldest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-langgraph-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-langgraph-latest", "ui_name": "loongsuite-instrumentation-langgraph-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-langgraph-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-langgraph-oldest", "ui_name": "loongsuite-instrumentation-langgraph-oldest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-langgraph-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-langgraph-latest", "ui_name": "loongsuite-instrumentation-langgraph-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-langgraph-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-langgraph-oldest", "ui_name": "loongsuite-instrumentation-langgraph-oldest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-langgraph-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-langgraph-latest", "ui_name": "loongsuite-instrumentation-langgraph-latest 3.13 ARC"}, {"name": "py311-test-loongsuite-instrumentation-deepagents-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-deepagents", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-deepagents-latest", "ui_name": "loongsuite-instrumentation-deepagents-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-deepagents-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-deepagents", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-deepagents-latest", "ui_name": "loongsuite-instrumentation-deepagents-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-deepagents-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-deepagents", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-deepagents-latest", "ui_name": "loongsuite-instrumentation-deepagents-latest 3.13 ARC"}, {"name": "py39-test-loongsuite-instrumentation-qwen-agent-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-qwen-agent-oldest", "ui_name": "loongsuite-instrumentation-qwen-agent-oldest 3.9 ARC"}, {"name": "py39-test-loongsuite-instrumentation-qwen-agent-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-qwen-agent-latest", "ui_name": "loongsuite-instrumentation-qwen-agent-latest 3.9 ARC"}, {"name": "py310-test-loongsuite-instrumentation-qwen-agent-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-qwen-agent-oldest", "ui_name": "loongsuite-instrumentation-qwen-agent-oldest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-qwen-agent-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-qwen-agent-latest", "ui_name": "loongsuite-instrumentation-qwen-agent-latest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-qwen-agent-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-qwen-agent-oldest", "ui_name": "loongsuite-instrumentation-qwen-agent-oldest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-qwen-agent-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-qwen-agent-latest", "ui_name": "loongsuite-instrumentation-qwen-agent-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-qwen-agent-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-qwen-agent-oldest", "ui_name": "loongsuite-instrumentation-qwen-agent-oldest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-qwen-agent-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-qwen-agent-latest", "ui_name": "loongsuite-instrumentation-qwen-agent-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-qwen-agent-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-qwen-agent-oldest", "ui_name": "loongsuite-instrumentation-qwen-agent-oldest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-qwen-agent-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-qwen-agent-latest", "ui_name": "loongsuite-instrumentation-qwen-agent-latest 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-hermes-agent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-hermes-agent", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-hermes-agent", "ui_name": "loongsuite-instrumentation-hermes-agent 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-hermes-agent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-hermes-agent", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-hermes-agent", "ui_name": "loongsuite-instrumentation-hermes-agent 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-hermes-agent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-hermes-agent", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-hermes-agent", "ui_name": "loongsuite-instrumentation-hermes-agent 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-hermes-agent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-hermes-agent", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-hermes-agent", "ui_name": "loongsuite-instrumentation-hermes-agent 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-mem0-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-mem0-oldest", "ui_name": "loongsuite-instrumentation-mem0-oldest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-mem0-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-mem0-latest", "ui_name": "loongsuite-instrumentation-mem0-latest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-mem0-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-mem0-oldest", "ui_name": "loongsuite-instrumentation-mem0-oldest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-mem0-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-mem0-latest", "ui_name": "loongsuite-instrumentation-mem0-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-mem0-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-mem0-oldest", "ui_name": "loongsuite-instrumentation-mem0-oldest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-mem0-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-mem0-latest", "ui_name": "loongsuite-instrumentation-mem0-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-mem0-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-mem0-oldest", "ui_name": "loongsuite-instrumentation-mem0-oldest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-mem0-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-mem0-latest", "ui_name": "loongsuite-instrumentation-mem0-latest 3.13 ARC"}, {"name": "py39-test-util-genai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "util-genai", "python_version": "3.9", "tox_env": "py39-test-util-genai", "ui_name": "util-genai 3.9 ARC"}, {"name": "py310-test-util-genai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "util-genai", "python_version": "3.10", "tox_env": "py310-test-util-genai", "ui_name": "util-genai 3.10 ARC"}, {"name": "py311-test-util-genai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "util-genai", "python_version": "3.11", "tox_env": "py311-test-util-genai", "ui_name": "util-genai 3.11 ARC"}, {"name": "py312-test-util-genai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "util-genai", "python_version": "3.12", "tox_env": "py312-test-util-genai", "ui_name": "util-genai 3.12 ARC"}, {"name": "py313-test-util-genai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "util-genai", "python_version": "3.13", "tox_env": "py313-test-util-genai", "ui_name": "util-genai 3.13 ARC"}, {"name": "py314-test-util-genai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "util-genai", "python_version": "3.14", "tox_env": "py314-test-util-genai", "ui_name": "util-genai 3.14 ARC"}, {"name": "pypy3-test-util-genai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "util-genai", "python_version": "pypy-3.9", "tox_env": "pypy3-test-util-genai", "ui_name": "util-genai pypy-3.9 ARC"}, {"name": "py311-test-detect-loongsuite-changes_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "detect-loongsuite-changes", "python_version": "3.11", "tox_env": "py311-test-detect-loongsuite-changes", "ui_name": "detect-loongsuite-changes 3.11 ARC"}, {"name": "py310-test-loongsuite-instrumentation-litellm_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-litellm", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-litellm", "ui_name": "loongsuite-instrumentation-litellm 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-litellm_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-litellm", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-litellm", "ui_name": "loongsuite-instrumentation-litellm 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-litellm_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-litellm", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-litellm", "ui_name": "loongsuite-instrumentation-litellm 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-litellm_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-litellm", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-litellm", "ui_name": "loongsuite-instrumentation-litellm 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-crewai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-crewai", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-crewai", "ui_name": "loongsuite-instrumentation-crewai 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-crewai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-crewai", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-crewai", "ui_name": "loongsuite-instrumentation-crewai 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-crewai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-crewai", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-crewai", "ui_name": "loongsuite-instrumentation-crewai 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-crewai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-crewai", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-crewai", "ui_name": "loongsuite-instrumentation-crewai 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-qwenpaw-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-qwenpaw-latest", "ui_name": "loongsuite-instrumentation-qwenpaw-latest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-qwenpaw-legacy_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-qwenpaw-legacy", "ui_name": "loongsuite-instrumentation-qwenpaw-legacy 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-qwenpaw-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-qwenpaw-latest", "ui_name": "loongsuite-instrumentation-qwenpaw-latest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-qwenpaw-legacy_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-qwenpaw-legacy", "ui_name": "loongsuite-instrumentation-qwenpaw-legacy 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-qwenpaw-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-qwenpaw-latest", "ui_name": "loongsuite-instrumentation-qwenpaw-latest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-qwenpaw-legacy_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-qwenpaw-legacy", "ui_name": "loongsuite-instrumentation-qwenpaw-legacy 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-qwenpaw-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-qwenpaw-latest", "ui_name": "loongsuite-instrumentation-qwenpaw-latest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-qwenpaw-legacy_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-qwenpaw-legacy", "ui_name": "loongsuite-instrumentation-qwenpaw-legacy 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-algotune_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-algotune", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-algotune", "ui_name": "loongsuite-instrumentation-algotune 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-algotune_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-algotune", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-algotune", "ui_name": "loongsuite-instrumentation-algotune 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-algotune_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-algotune", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-algotune", "ui_name": "loongsuite-instrumentation-algotune 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-algotune_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-algotune", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-algotune", "ui_name": "loongsuite-instrumentation-algotune 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-bfclv4_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-bfclv4", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-bfclv4", "ui_name": "loongsuite-instrumentation-bfclv4 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-bfclv4_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-bfclv4", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-bfclv4", "ui_name": "loongsuite-instrumentation-bfclv4 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-bfclv4_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-bfclv4", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-bfclv4", "ui_name": "loongsuite-instrumentation-bfclv4 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-bfclv4_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-bfclv4", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-bfclv4", "ui_name": "loongsuite-instrumentation-bfclv4 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-claw-eval_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claw-eval", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-claw-eval", "ui_name": "loongsuite-instrumentation-claw-eval 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-claw-eval_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claw-eval", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-claw-eval", "ui_name": "loongsuite-instrumentation-claw-eval 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-claw-eval_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claw-eval", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-claw-eval", "ui_name": "loongsuite-instrumentation-claw-eval 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-claw-eval_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claw-eval", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-claw-eval", "ui_name": "loongsuite-instrumentation-claw-eval 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-minisweagent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-minisweagent", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-minisweagent", "ui_name": "loongsuite-instrumentation-minisweagent 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-minisweagent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-minisweagent", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-minisweagent", "ui_name": "loongsuite-instrumentation-minisweagent 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-minisweagent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-minisweagent", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-minisweagent", "ui_name": "loongsuite-instrumentation-minisweagent 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-minisweagent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-minisweagent", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-minisweagent", "ui_name": "loongsuite-instrumentation-minisweagent 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-openhands_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-openhands", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-openhands", "ui_name": "loongsuite-instrumentation-openhands 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-openhands_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-openhands", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-openhands", "ui_name": "loongsuite-instrumentation-openhands 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-openhands_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-openhands", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-openhands", "ui_name": "loongsuite-instrumentation-openhands 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-openhands_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-openhands", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-openhands", "ui_name": "loongsuite-instrumentation-openhands 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-slop-code_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-slop-code", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-slop-code", "ui_name": "loongsuite-instrumentation-slop-code 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-slop-code_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-slop-code", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-slop-code", "ui_name": "loongsuite-instrumentation-slop-code 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-slop-code_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-slop-code", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-slop-code", "ui_name": "loongsuite-instrumentation-slop-code 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-slop-code_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-slop-code", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-slop-code", "ui_name": "loongsuite-instrumentation-slop-code 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-terminus2_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-terminus2", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-terminus2", "ui_name": "loongsuite-instrumentation-terminus2 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-terminus2_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-terminus2", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-terminus2", "ui_name": "loongsuite-instrumentation-terminus2 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-terminus2_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-terminus2", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-terminus2", "ui_name": "loongsuite-instrumentation-terminus2 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-terminus2_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-terminus2", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-terminus2", "ui_name": "loongsuite-instrumentation-terminus2 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-vita_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-vita", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-vita", "ui_name": "loongsuite-instrumentation-vita 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-vita_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-vita", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-vita", "ui_name": "loongsuite-instrumentation-vita 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-vita_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-vita", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-vita", "ui_name": "loongsuite-instrumentation-vita 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-vita_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-vita", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-vita", "ui_name": "loongsuite-instrumentation-vita 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-webarena_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-webarena", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-webarena", "ui_name": "loongsuite-instrumentation-webarena 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-webarena_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-webarena", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-webarena", "ui_name": "loongsuite-instrumentation-webarena 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-webarena_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-webarena", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-webarena", "ui_name": "loongsuite-instrumentation-webarena 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-webarena_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-webarena", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-webarena", "ui_name": "loongsuite-instrumentation-webarena 3.13 ARC"}, {"name": "py311-test-loongsuite-instrumentation-widesearch_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-widesearch", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-widesearch", "ui_name": "loongsuite-instrumentation-widesearch 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-widesearch_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-widesearch", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-widesearch", "ui_name": "loongsuite-instrumentation-widesearch 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-widesearch_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-widesearch", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-widesearch", "ui_name": "loongsuite-instrumentation-widesearch 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-wildtool_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-wildtool", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-wildtool", "ui_name": "loongsuite-instrumentation-wildtool 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-wildtool_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-wildtool", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-wildtool", "ui_name": "loongsuite-instrumentation-wildtool 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-wildtool_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-wildtool", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-wildtool", "ui_name": "loongsuite-instrumentation-wildtool 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-wildtool_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-wildtool", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-wildtool", "ui_name": "loongsuite-instrumentation-wildtool 3.13 ARC"}] + [{"name": "py310-test-loongsuite-instrumentation-agentscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-agentscope-oldest", "ui_name": "loongsuite-instrumentation-agentscope-oldest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-agentscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-agentscope-oldest", "ui_name": "loongsuite-instrumentation-agentscope-oldest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-agentscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-agentscope-oldest", "ui_name": "loongsuite-instrumentation-agentscope-oldest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-agentscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-agentscope-oldest", "ui_name": "loongsuite-instrumentation-agentscope-oldest 3.13 ARC"}, {"name": "py311-test-loongsuite-instrumentation-agentscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-agentscope-latest", "ui_name": "loongsuite-instrumentation-agentscope-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-agentscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-agentscope-latest", "ui_name": "loongsuite-instrumentation-agentscope-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-agentscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-agentscope-latest", "ui_name": "loongsuite-instrumentation-agentscope-latest 3.13 ARC"}, {"name": "py39-test-loongsuite-instrumentation-dashscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-dashscope-oldest", "ui_name": "loongsuite-instrumentation-dashscope-oldest 3.9 ARC"}, {"name": "py39-test-loongsuite-instrumentation-dashscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-dashscope-latest", "ui_name": "loongsuite-instrumentation-dashscope-latest 3.9 ARC"}, {"name": "py310-test-loongsuite-instrumentation-dashscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-dashscope-oldest", "ui_name": "loongsuite-instrumentation-dashscope-oldest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-dashscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-dashscope-latest", "ui_name": "loongsuite-instrumentation-dashscope-latest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-dashscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-dashscope-oldest", "ui_name": "loongsuite-instrumentation-dashscope-oldest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-dashscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-dashscope-latest", "ui_name": "loongsuite-instrumentation-dashscope-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-dashscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-dashscope-oldest", "ui_name": "loongsuite-instrumentation-dashscope-oldest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-dashscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-dashscope-latest", "ui_name": "loongsuite-instrumentation-dashscope-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-dashscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-dashscope-oldest", "ui_name": "loongsuite-instrumentation-dashscope-oldest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-dashscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-dashscope-latest", "ui_name": "loongsuite-instrumentation-dashscope-latest 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-claude-agent-sdk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-claude-agent-sdk-oldest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-oldest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-claude-agent-sdk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-claude-agent-sdk-latest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-latest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-claude-agent-sdk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-claude-agent-sdk-oldest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-oldest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-claude-agent-sdk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-claude-agent-sdk-latest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-claude-agent-sdk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-claude-agent-sdk-oldest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-oldest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-claude-agent-sdk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-claude-agent-sdk-latest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-claude-agent-sdk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-claude-agent-sdk-oldest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-oldest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-claude-agent-sdk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-claude-agent-sdk-latest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-latest 3.13 ARC"}, {"name": "py39-test-loongsuite-instrumentation-google-adk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-google-adk-oldest", "ui_name": "loongsuite-instrumentation-google-adk-oldest 3.9 ARC"}, {"name": "py39-test-loongsuite-instrumentation-google-adk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-google-adk-latest", "ui_name": "loongsuite-instrumentation-google-adk-latest 3.9 ARC"}, {"name": "py310-test-loongsuite-instrumentation-google-adk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-google-adk-oldest", "ui_name": "loongsuite-instrumentation-google-adk-oldest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-google-adk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-google-adk-latest", "ui_name": "loongsuite-instrumentation-google-adk-latest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-google-adk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-google-adk-oldest", "ui_name": "loongsuite-instrumentation-google-adk-oldest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-google-adk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-google-adk-latest", "ui_name": "loongsuite-instrumentation-google-adk-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-google-adk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-google-adk-oldest", "ui_name": "loongsuite-instrumentation-google-adk-oldest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-google-adk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-google-adk-latest", "ui_name": "loongsuite-instrumentation-google-adk-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-google-adk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-google-adk-oldest", "ui_name": "loongsuite-instrumentation-google-adk-oldest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-google-adk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-google-adk-latest", "ui_name": "loongsuite-instrumentation-google-adk-latest 3.13 ARC"}, {"name": "py39-test-loongsuite-instrumentation-agno_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agno", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-agno", "ui_name": "loongsuite-instrumentation-agno 3.9 ARC"}, {"name": "py310-test-loongsuite-instrumentation-agno_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agno", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-agno", "ui_name": "loongsuite-instrumentation-agno 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-agno_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agno", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-agno", "ui_name": "loongsuite-instrumentation-agno 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-agno_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agno", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-agno", "ui_name": "loongsuite-instrumentation-agno 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-agno_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agno", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-agno", "ui_name": "loongsuite-instrumentation-agno 3.13 ARC"}, {"name": "py39-test-loongsuite-instrumentation-langchain-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-langchain-oldest", "ui_name": "loongsuite-instrumentation-langchain-oldest 3.9 ARC"}, {"name": "py39-test-loongsuite-instrumentation-langchain-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-langchain-latest", "ui_name": "loongsuite-instrumentation-langchain-latest 3.9 ARC"}, {"name": "py310-test-loongsuite-instrumentation-langchain-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-langchain-oldest", "ui_name": "loongsuite-instrumentation-langchain-oldest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-langchain-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-langchain-latest", "ui_name": "loongsuite-instrumentation-langchain-latest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-langchain-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-langchain-oldest", "ui_name": "loongsuite-instrumentation-langchain-oldest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-langchain-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-langchain-latest", "ui_name": "loongsuite-instrumentation-langchain-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-langchain-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-langchain-oldest", "ui_name": "loongsuite-instrumentation-langchain-oldest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-langchain-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-langchain-latest", "ui_name": "loongsuite-instrumentation-langchain-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-langchain-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-langchain-oldest", "ui_name": "loongsuite-instrumentation-langchain-oldest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-langchain-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-langchain-latest", "ui_name": "loongsuite-instrumentation-langchain-latest 3.13 ARC"}, {"name": "py39-test-loongsuite-instrumentation-langgraph-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-langgraph-oldest", "ui_name": "loongsuite-instrumentation-langgraph-oldest 3.9 ARC"}, {"name": "py39-test-loongsuite-instrumentation-langgraph-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-langgraph-latest", "ui_name": "loongsuite-instrumentation-langgraph-latest 3.9 ARC"}, {"name": "py310-test-loongsuite-instrumentation-langgraph-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-langgraph-oldest", "ui_name": "loongsuite-instrumentation-langgraph-oldest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-langgraph-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-langgraph-latest", "ui_name": "loongsuite-instrumentation-langgraph-latest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-langgraph-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-langgraph-oldest", "ui_name": "loongsuite-instrumentation-langgraph-oldest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-langgraph-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-langgraph-latest", "ui_name": "loongsuite-instrumentation-langgraph-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-langgraph-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-langgraph-oldest", "ui_name": "loongsuite-instrumentation-langgraph-oldest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-langgraph-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-langgraph-latest", "ui_name": "loongsuite-instrumentation-langgraph-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-langgraph-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-langgraph-oldest", "ui_name": "loongsuite-instrumentation-langgraph-oldest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-langgraph-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-langgraph-latest", "ui_name": "loongsuite-instrumentation-langgraph-latest 3.13 ARC"}, {"name": "py311-test-loongsuite-instrumentation-deepagents-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-deepagents", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-deepagents-latest", "ui_name": "loongsuite-instrumentation-deepagents-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-deepagents-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-deepagents", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-deepagents-latest", "ui_name": "loongsuite-instrumentation-deepagents-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-deepagents-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-deepagents", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-deepagents-latest", "ui_name": "loongsuite-instrumentation-deepagents-latest 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-microsoft-agent-framework_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-microsoft-agent-framework", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-microsoft-agent-framework", "ui_name": "loongsuite-instrumentation-microsoft-agent-framework 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-microsoft-agent-framework_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-microsoft-agent-framework", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-microsoft-agent-framework", "ui_name": "loongsuite-instrumentation-microsoft-agent-framework 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-microsoft-agent-framework_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-microsoft-agent-framework", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-microsoft-agent-framework", "ui_name": "loongsuite-instrumentation-microsoft-agent-framework 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-microsoft-agent-framework_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-microsoft-agent-framework", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-microsoft-agent-framework", "ui_name": "loongsuite-instrumentation-microsoft-agent-framework 3.13 ARC"}, {"name": "py39-test-loongsuite-instrumentation-qwen-agent-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-qwen-agent-oldest", "ui_name": "loongsuite-instrumentation-qwen-agent-oldest 3.9 ARC"}, {"name": "py39-test-loongsuite-instrumentation-qwen-agent-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-qwen-agent-latest", "ui_name": "loongsuite-instrumentation-qwen-agent-latest 3.9 ARC"}, {"name": "py310-test-loongsuite-instrumentation-qwen-agent-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-qwen-agent-oldest", "ui_name": "loongsuite-instrumentation-qwen-agent-oldest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-qwen-agent-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-qwen-agent-latest", "ui_name": "loongsuite-instrumentation-qwen-agent-latest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-qwen-agent-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-qwen-agent-oldest", "ui_name": "loongsuite-instrumentation-qwen-agent-oldest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-qwen-agent-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-qwen-agent-latest", "ui_name": "loongsuite-instrumentation-qwen-agent-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-qwen-agent-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-qwen-agent-oldest", "ui_name": "loongsuite-instrumentation-qwen-agent-oldest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-qwen-agent-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-qwen-agent-latest", "ui_name": "loongsuite-instrumentation-qwen-agent-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-qwen-agent-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-qwen-agent-oldest", "ui_name": "loongsuite-instrumentation-qwen-agent-oldest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-qwen-agent-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-qwen-agent-latest", "ui_name": "loongsuite-instrumentation-qwen-agent-latest 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-hermes-agent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-hermes-agent", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-hermes-agent", "ui_name": "loongsuite-instrumentation-hermes-agent 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-hermes-agent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-hermes-agent", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-hermes-agent", "ui_name": "loongsuite-instrumentation-hermes-agent 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-hermes-agent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-hermes-agent", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-hermes-agent", "ui_name": "loongsuite-instrumentation-hermes-agent 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-hermes-agent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-hermes-agent", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-hermes-agent", "ui_name": "loongsuite-instrumentation-hermes-agent 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-mem0-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-mem0-oldest", "ui_name": "loongsuite-instrumentation-mem0-oldest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-mem0-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-mem0-latest", "ui_name": "loongsuite-instrumentation-mem0-latest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-mem0-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-mem0-oldest", "ui_name": "loongsuite-instrumentation-mem0-oldest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-mem0-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-mem0-latest", "ui_name": "loongsuite-instrumentation-mem0-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-mem0-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-mem0-oldest", "ui_name": "loongsuite-instrumentation-mem0-oldest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-mem0-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-mem0-latest", "ui_name": "loongsuite-instrumentation-mem0-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-mem0-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-mem0-oldest", "ui_name": "loongsuite-instrumentation-mem0-oldest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-mem0-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-mem0-latest", "ui_name": "loongsuite-instrumentation-mem0-latest 3.13 ARC"}, {"name": "py39-test-util-genai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "util-genai", "python_version": "3.9", "tox_env": "py39-test-util-genai", "ui_name": "util-genai 3.9 ARC"}, {"name": "py310-test-util-genai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "util-genai", "python_version": "3.10", "tox_env": "py310-test-util-genai", "ui_name": "util-genai 3.10 ARC"}, {"name": "py311-test-util-genai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "util-genai", "python_version": "3.11", "tox_env": "py311-test-util-genai", "ui_name": "util-genai 3.11 ARC"}, {"name": "py312-test-util-genai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "util-genai", "python_version": "3.12", "tox_env": "py312-test-util-genai", "ui_name": "util-genai 3.12 ARC"}, {"name": "py313-test-util-genai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "util-genai", "python_version": "3.13", "tox_env": "py313-test-util-genai", "ui_name": "util-genai 3.13 ARC"}, {"name": "py314-test-util-genai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "util-genai", "python_version": "3.14", "tox_env": "py314-test-util-genai", "ui_name": "util-genai 3.14 ARC"}, {"name": "pypy3-test-util-genai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "util-genai", "python_version": "pypy-3.9", "tox_env": "pypy3-test-util-genai", "ui_name": "util-genai pypy-3.9 ARC"}, {"name": "py311-test-detect-loongsuite-changes_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "detect-loongsuite-changes", "python_version": "3.11", "tox_env": "py311-test-detect-loongsuite-changes", "ui_name": "detect-loongsuite-changes 3.11 ARC"}, {"name": "py310-test-loongsuite-instrumentation-litellm_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-litellm", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-litellm", "ui_name": "loongsuite-instrumentation-litellm 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-litellm_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-litellm", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-litellm", "ui_name": "loongsuite-instrumentation-litellm 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-litellm_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-litellm", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-litellm", "ui_name": "loongsuite-instrumentation-litellm 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-litellm_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-litellm", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-litellm", "ui_name": "loongsuite-instrumentation-litellm 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-crewai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-crewai", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-crewai", "ui_name": "loongsuite-instrumentation-crewai 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-crewai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-crewai", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-crewai", "ui_name": "loongsuite-instrumentation-crewai 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-crewai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-crewai", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-crewai", "ui_name": "loongsuite-instrumentation-crewai 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-crewai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-crewai", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-crewai", "ui_name": "loongsuite-instrumentation-crewai 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-qwenpaw-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-qwenpaw-latest", "ui_name": "loongsuite-instrumentation-qwenpaw-latest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-qwenpaw-legacy_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-qwenpaw-legacy", "ui_name": "loongsuite-instrumentation-qwenpaw-legacy 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-qwenpaw-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-qwenpaw-latest", "ui_name": "loongsuite-instrumentation-qwenpaw-latest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-qwenpaw-legacy_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-qwenpaw-legacy", "ui_name": "loongsuite-instrumentation-qwenpaw-legacy 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-qwenpaw-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-qwenpaw-latest", "ui_name": "loongsuite-instrumentation-qwenpaw-latest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-qwenpaw-legacy_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-qwenpaw-legacy", "ui_name": "loongsuite-instrumentation-qwenpaw-legacy 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-qwenpaw-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-qwenpaw-latest", "ui_name": "loongsuite-instrumentation-qwenpaw-latest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-qwenpaw-legacy_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-qwenpaw-legacy", "ui_name": "loongsuite-instrumentation-qwenpaw-legacy 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-algotune_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-algotune", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-algotune", "ui_name": "loongsuite-instrumentation-algotune 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-algotune_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-algotune", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-algotune", "ui_name": "loongsuite-instrumentation-algotune 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-algotune_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-algotune", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-algotune", "ui_name": "loongsuite-instrumentation-algotune 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-algotune_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-algotune", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-algotune", "ui_name": "loongsuite-instrumentation-algotune 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-bfclv4_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-bfclv4", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-bfclv4", "ui_name": "loongsuite-instrumentation-bfclv4 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-bfclv4_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-bfclv4", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-bfclv4", "ui_name": "loongsuite-instrumentation-bfclv4 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-bfclv4_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-bfclv4", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-bfclv4", "ui_name": "loongsuite-instrumentation-bfclv4 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-bfclv4_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-bfclv4", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-bfclv4", "ui_name": "loongsuite-instrumentation-bfclv4 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-claw-eval_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claw-eval", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-claw-eval", "ui_name": "loongsuite-instrumentation-claw-eval 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-claw-eval_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claw-eval", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-claw-eval", "ui_name": "loongsuite-instrumentation-claw-eval 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-claw-eval_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claw-eval", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-claw-eval", "ui_name": "loongsuite-instrumentation-claw-eval 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-claw-eval_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claw-eval", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-claw-eval", "ui_name": "loongsuite-instrumentation-claw-eval 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-minisweagent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-minisweagent", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-minisweagent", "ui_name": "loongsuite-instrumentation-minisweagent 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-minisweagent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-minisweagent", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-minisweagent", "ui_name": "loongsuite-instrumentation-minisweagent 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-minisweagent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-minisweagent", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-minisweagent", "ui_name": "loongsuite-instrumentation-minisweagent 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-minisweagent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-minisweagent", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-minisweagent", "ui_name": "loongsuite-instrumentation-minisweagent 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-openhands_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-openhands", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-openhands", "ui_name": "loongsuite-instrumentation-openhands 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-openhands_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-openhands", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-openhands", "ui_name": "loongsuite-instrumentation-openhands 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-openhands_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-openhands", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-openhands", "ui_name": "loongsuite-instrumentation-openhands 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-openhands_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-openhands", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-openhands", "ui_name": "loongsuite-instrumentation-openhands 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-slop-code_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-slop-code", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-slop-code", "ui_name": "loongsuite-instrumentation-slop-code 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-slop-code_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-slop-code", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-slop-code", "ui_name": "loongsuite-instrumentation-slop-code 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-slop-code_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-slop-code", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-slop-code", "ui_name": "loongsuite-instrumentation-slop-code 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-slop-code_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-slop-code", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-slop-code", "ui_name": "loongsuite-instrumentation-slop-code 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-terminus2_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-terminus2", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-terminus2", "ui_name": "loongsuite-instrumentation-terminus2 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-terminus2_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-terminus2", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-terminus2", "ui_name": "loongsuite-instrumentation-terminus2 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-terminus2_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-terminus2", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-terminus2", "ui_name": "loongsuite-instrumentation-terminus2 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-terminus2_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-terminus2", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-terminus2", "ui_name": "loongsuite-instrumentation-terminus2 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-vita_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-vita", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-vita", "ui_name": "loongsuite-instrumentation-vita 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-vita_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-vita", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-vita", "ui_name": "loongsuite-instrumentation-vita 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-vita_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-vita", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-vita", "ui_name": "loongsuite-instrumentation-vita 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-vita_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-vita", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-vita", "ui_name": "loongsuite-instrumentation-vita 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-webarena_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-webarena", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-webarena", "ui_name": "loongsuite-instrumentation-webarena 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-webarena_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-webarena", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-webarena", "ui_name": "loongsuite-instrumentation-webarena 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-webarena_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-webarena", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-webarena", "ui_name": "loongsuite-instrumentation-webarena 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-webarena_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-webarena", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-webarena", "ui_name": "loongsuite-instrumentation-webarena 3.13 ARC"}, {"name": "py311-test-loongsuite-instrumentation-widesearch_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-widesearch", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-widesearch", "ui_name": "loongsuite-instrumentation-widesearch 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-widesearch_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-widesearch", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-widesearch", "ui_name": "loongsuite-instrumentation-widesearch 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-widesearch_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-widesearch", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-widesearch", "ui_name": "loongsuite-instrumentation-widesearch 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-wildtool_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-wildtool", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-wildtool", "ui_name": "loongsuite-instrumentation-wildtool 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-wildtool_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-wildtool", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-wildtool", "ui_name": "loongsuite-instrumentation-wildtool 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-wildtool_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-wildtool", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-wildtool", "ui_name": "loongsuite-instrumentation-wildtool 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-wildtool_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-wildtool", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-wildtool", "ui_name": "loongsuite-instrumentation-wildtool 3.13 ARC"}] LOONGSUITE_FULL: ${{ steps.detect.outputs.full }} LOONGSUITE_PACKAGES: ${{ steps.detect.outputs.packages }} run: | diff --git a/instrumentation-loongsuite/README.md b/instrumentation-loongsuite/README.md index 8c6732cee..397ec9c31 100644 --- a/instrumentation-loongsuite/README.md +++ b/instrumentation-loongsuite/README.md @@ -4,11 +4,13 @@ | [loongsuite-instrumentation-agentscope](./loongsuite-instrumentation-agentscope) | agentscope >= 1.0.0, < 3.0.0 | No | development | [loongsuite-instrumentation-agno](./loongsuite-instrumentation-agno) | agno >= 2.0.0, < 3 | No | development | [loongsuite-instrumentation-algotune](./loongsuite-instrumentation-algotune) | algotune | No | development +| [loongsuite-instrumentation-autogen](./loongsuite-instrumentation-autogen) | autogen-agentchat >= 0.7.0, < 0.8.0 | No | development | [loongsuite-instrumentation-bfclv4](./loongsuite-instrumentation-bfclv4) | bfcl-eval >= 4.0.0 | No | development | [loongsuite-instrumentation-claude-agent-sdk](./loongsuite-instrumentation-claude-agent-sdk) | claude-agent-sdk >= 0.1.0 | No | development | [loongsuite-instrumentation-claw-eval](./loongsuite-instrumentation-claw-eval) | claw-eval >= 0.1.0 | No | development | [loongsuite-instrumentation-crewai](./loongsuite-instrumentation-crewai) | crewai >= 0.80.0 | No | development | [loongsuite-instrumentation-dashscope](./loongsuite-instrumentation-dashscope) | dashscope >= 1.0.0 | No | development +| [loongsuite-instrumentation-deepagents](./loongsuite-instrumentation-deepagents) | deepagents >= 0.6.0, < 0.7.0 | No | development | [loongsuite-instrumentation-dify](./loongsuite-instrumentation-dify) | dify | No | development | [loongsuite-instrumentation-google-adk](./loongsuite-instrumentation-google-adk) | google-adk >= 0.1.0 | No | development | [loongsuite-instrumentation-hermes-agent](./loongsuite-instrumentation-hermes-agent) | openai >= 1.0.0 | No | development @@ -17,6 +19,7 @@ | [loongsuite-instrumentation-litellm](./loongsuite-instrumentation-litellm) | litellm >= 1.0.0 | No | development | [loongsuite-instrumentation-mcp](./loongsuite-instrumentation-mcp) | mcp >= 1.3.0, <= 1.25.0 | No | development | [loongsuite-instrumentation-mem0](./loongsuite-instrumentation-mem0) | mem0ai >= 1.0.0, < 2.0.0 | No | development +| [loongsuite-instrumentation-microsoft-agent-framework](./loongsuite-instrumentation-microsoft-agent-framework) | agent-framework-core >= 1.0.0 | No | development | [loongsuite-instrumentation-minisweagent](./loongsuite-instrumentation-minisweagent) | mini-swe-agent >= 2.2.0 | No | development | [loongsuite-instrumentation-openhands](./loongsuite-instrumentation-openhands) | openhands | No | development | [loongsuite-instrumentation-qwen-agent](./loongsuite-instrumentation-qwen-agent) | qwen-agent >= 0.0.20 | No | development diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_agentscope.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_agentscope.py index 67366a8e5..31ba7075b 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_agentscope.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_agentscope.py @@ -24,7 +24,7 @@ "package": "loongsuite-instrumentation-agentscope", "instrumentation": "loongsuite-instrumentation-agentscope==0.7.0.dev", "libraries": [ - "agentscope >= 1.0.0", + "agentscope >= 1.0.0, < 3.0.0", ], "default": False, } diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_microsoft_agent_framework.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_microsoft_agent_framework.py new file mode 100644 index 000000000..b6416e669 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_microsoft_agent_framework.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "loongsuite", + "package": "loongsuite-instrumentation-microsoft-agent-framework", + "instrumentation": "loongsuite-instrumentation-microsoft-agent-framework==0.7.0.dev", + "libraries": [ + "agent-framework-core >= 1.0.0", + ], + "default": False, +}