Skip to content

feat(google-genai): add util-backed 2.x instrumentation - #250

Open
sipercai wants to merge 7 commits into
mainfrom
feat/google-genai-observability
Open

feat(google-genai): add util-backed 2.x instrumentation#250
sipercai wants to merge 7 commits into
mainfrom
feat/google-genai-observability

Conversation

@sipercai

@sipercai sipercai commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Description

This PR adds LoongSuite instrumentation for the Google Gen AI Python SDK 2.x, backed by opentelemetry-util-genai.

The provider implementation is aligned with the canonical OpenTelemetry Python GenAI Google instrumentation (1.0b1/current upstream snapshot), while retaining LoongSuite's shared GenAI util for completion hooks, content capture, multimodal extensions, and client metrics.

Supported synchronous and asynchronous surfaces include:

  • models.generate_content
  • models.generate_content_stream
  • models.embed_content
  • interactions.create, including streaming
  • automatic Python function tools

The instrumentation records LLM, EMBEDDING, and TOOL spans; request/response metadata; response IDs; finish reasons; token usage; captured messages; streaming TTFT; embedding dimensions; errors; and GenAI client metrics.

After PR #245, instrumentation-only work is isolated with hook_advice: provider/application calls and stream iteration execute exactly once outside advice, while telemetry prepare, mapping, chunk, close/finalize, and wrapper-construction failures degrade telemetry without changing the provider result, chunk identity/order, cancellation, GeneratorExit, or original exception.

Google ADK changes and Robin's commercial _SUPPRESS_LLM_SDK_KEY overlay are intentionally out of scope.

Fixes # (N/A)

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

  • Python 3.9–3.13, oldest/latest Google GenAI dependency matrix
  • 205 passed, 95 skipped on the latest package environment
  • Dedicated fail-open tests for prepare/mapping/finalizer/reporter/chunk/wrapper/close/aclose/cancellation/GeneratorExit/cross-context/concurrency/tool failures
  • Fresh-cache tox -e precommit
  • tox -e generate-workflows
  • Package wheel and sdist build
  • Sanitized cassette replay with --vcr-record=none
  • Bounded live google-genai 2.14.0 Gemini verification with CMS readback

The live matrix covered sync/async non-streaming, sync/async streaming, embeddings, interactions, interaction streaming, deterministic function tools, multi-turn calls, two concurrent clients, early close/aclose, a real provider error, and injected response/chunk telemetry failures. CMS read back all 20 scenario traces and all 42 exported spans, including LLM → TOOL, EMBEDDING, TTFT, token usage, and ERROR trees.

Does This PR Require a Core Repo Change?

  • Yes. - Link to PR:
  • No.

Checklist:

See contributing.md for styleguide, changelog guidelines, and more.

  • Followed the style guidelines of this project
  • Changelogs have been updated
  • Unit tests have been added
  • Documentation has been updated

Validation Evidence

Spec and Scope

  • Direct feature implementation scoped to the Google Gen AI SDK provider plugin.
  • Canonical upstream baseline and delta are documented in UPSTREAM.md.
  • No Google ADK or Robin commercial behavior is included.

Local Checks

Check Result
Static readiness pass
Python 3.9–3.13 oldest/latest tox matrix pass
Latest focused tests 205 passed, 95 skipped
Package Ruff lint pass
Fresh-cache precommit pass
Workflow generation pass, generated files stable
Wheel and sdist build pass
Cassette privacy scan pass, zero credential/path matches

Real E2E Matrix

Scenario Result Observed telemetry
sync/async generation pass LLM response/model/token attributes
sync/async SSE pass ordered chunks, TTFT, token usage
sync/async embeddings pass EMBEDDING spans and dimensions
sync/async interactions pass interactions.create LLM spans
deterministic function tool pass LLM parent with TOOL child
multi-turn pass two sibling LLM spans under one scenario
two concurrent clients pass isolated trace/context state
early close/aclose pass span finalized once
invalid provider model pass original ClientError, ERROR telemetry
response-mapping advice failure pass original response preserved, telemetry safely degraded
every-chunk advice failure pass all provider chunks preserved, telemetry safely degraded

Upstream comparison

Both upstream implementations contain useful local span-isolation patterns, but still mix some request/stream telemetry callbacks with the business boundary. The LoongSuite delta keeps the upstream provider mapping while applying the stricter PR #245 fail-open lifecycle.

@sipercai
sipercai force-pushed the feat/google-genai-observability branch from d18ea4c to 4f9a4ba Compare July 28, 2026 07:23
@sipercai
sipercai marked this pull request as ready for review July 28, 2026 08:08
server_url = getattr(config, "server_url", "")
if server_url:
server_address = server_url
if "aiplatform.googleapis.com" in server_url:

@ralf0131 ralf0131 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR adds comprehensive Google GenAI Python SDK 2.x instrumentation backed by opentelemetry-util-genai. The implementation follows the established LoongSuite instrumentation patterns, supports both sync/async surfaces (generate_content, embed_content, interactions), and includes thorough fail-open error handling via the hook_advice mechanism. Test coverage is extensive (205 passed, 95 skipped) with dedicated fail-open tests for every telemetry failure mode.

Findings

  • [Info] generate_content.py:1 — 1021-line file could benefit from splitting mapping helpers into a separate module
  • [Info] _compat.py:31 — Bridge module is well-documented; consider a greppable TODO marker for the cleanup target
  • [Warning] _compat.py:218 — Reasoning tokens silently added to output_tokens total; verify this matches the GenAI semantic convention intent
  • [Info] _stream.py:97_StreamWrapperMeta metaclass pattern is clean and well-commented
  • [Info] generate_content.py:69 — MCP import guard is correct for typical install-time dependency resolution

Suggestions

  1. File size: Consider extracting _map_generate_content_config, _extract_* helpers and the _MessageConverter into a _mapping.py module. This would make generate_content.py focus purely on the instrumentation lifecycle.
  2. Reasoning token accounting: Add a brief comment or assertion clarifying whether output_tokens should include or exclude reasoning tokens per the semantic conventions.
  3. UPSTREAM.md: The upstream alignment documentation is excellent. Consider keeping it updated as the upstream 1.0b1 evolves.

Overall

Solid, well-tested instrumentation that aligns with both upstream OTel conventions and LoongSuite's existing patterns. The fail-open design via hook_advice is consistently applied across all surfaces. No blocking issues — the findings above are informational.


Automated review by github-manager-bot

@@ -0,0 +1,1021 @@
# Copyright The OpenTelemetry Authors

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Info] This file is 1021 lines — consider splitting the _GenerateContentHandler (span/metric lifecycle) from the pure mapping helpers (_map_*, _extract_*) into separate modules. The mapping functions are stateless and testable in isolation; separating them would reduce cognitive load when navigating the instrumentation lifecycle.

import timeit
from dataclasses import asdict, dataclass
from types import TracebackType
from typing import Any

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Info] The bridge pattern here is well-documented — the module docstring clearly explains this is a removable impedance match. One suggestion: consider adding a # TODO(loongsuite-util-migration): remove after util migrates to 1.x marker at the module level so the cleanup target is greppable.

(_OUTPUT_MESSAGES, self.output_messages),
(_SYSTEM_INSTRUCTIONS, self.system_instruction),
)
for key, messages in message_attributes:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Warning] In _prepare_finish, self.output_tokens = (self.output_tokens or 0) + self.thinking_tokens silently adds reasoning tokens to the total output token count. This is correct if the upstream semantic convention defines output_tokens as inclusive of reasoning, but if it should be exclusive, this would inflate the reported count. Worth confirming against the GenAI semantic conventions spec — or adding a comment clarifying the intended behavior.

``for chunk in wrapper`` or ``with wrapper``. The hook methods are called
internally by the wrapper lifecycle and are not part of the public API.
"""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Info] The _StreamWrapperMeta metaclass resolving both ABCMeta and wrapt.ObjectProxy's metaclass is a clean solution. The comment explaining why this is needed is helpful for future maintainers.

from .tool_call_wrapper import wrapped_tool

_is_mcp_imported = False
McpClientSession = McpTool = None

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Info] The MCP client import guard (_is_mcp_imported) is correctly placed at module level with try/except ImportError. One minor note: the global _is_mcp_imported flag is set to False initially and then potentially overwritten. If this module is imported before mcp is installed but mcp is installed later at runtime, the flag won't update. This is fine for the typical use case (dependencies are fixed at install time), but worth being aware of in dynamic environments.

@ralf0131 ralf0131 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the core instrumentation structure. The implementation follows established OTel instrumentation patterns with clean separation of concerns:

Architecture

  • instrumentor.py follows BaseInstrumentor contract correctly
  • _compat.py bridge layer properly adapts upstream 1.0 API to LoongSuite extensions
  • _stream.py handles sync/async streams with proper context manager protocol and finalization
  • generate_content.py covers sync/async generate_content, embed_content, and interactions

Key Observations

  • AllowList utility supports regex patterns with proper escaping
  • Stream wrappers use wrapt.ObjectProxy with correct metaclass for type compatibility
  • Token usage accounting and TTFT tracking are properly integrated
  • Test coverage is comprehensive with VCR cassettes for deterministic replay

Minor Note

  • Large PR (10K+ lines) — maintainer may want to verify upstream semantic convention alignment separately

Overall the code is well-structured and follows the project's established patterns.


Automated review by github-manager-bot

@ralf0131

Copy link
Copy Markdown
Collaborator

⚠️ Merge conflict detected

This PR has conflicts with the main branch and cannot be merged. Please rebase or merge main into your branch and resolve the conflicts:

git fetch origin
git checkout feat/google-genai-observability
git rebase origin/main
# resolve conflicts, then:
git push --force-with-lease

This is a one-time reminder. Feel free to @mention me for a re-review after conflicts are resolved.


Automated notification by github-manager-bot

@ralf0131 ralf0131 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Comprehensive Google GenAI SDK 2.x instrumentation package aligned with upstream OTel opentelemetry-instrumentation-google-genai==1.0b1. Well-structured with proper fail-open semantics, sync/async parity, streaming TTFT support, embeddings, interactions, and automatic function tool wrapping. Test coverage (205 passed, 95 skipped) is thorough including fail-open edge cases.

Findings

  • [Info] generate_content.py is 1021 lines — consider splitting _instrument_generate_content and _uninstrument_generate_content into a separate module if it grows further, to keep the file navigable.
  • [Info] The _is_mcp_imported module-level flag with McpClientSession/McpTool globals is fine for optional dependency handling, but worth noting that importing mcp at module load time could trigger side effects in environments where mcp is installed but not intended for use with this instrumentor.

Suggestions

  • The UPSTREAM.md baseline documentation is excellent. Consider adding a brief note on the divergence points (e.g., hook_advice integration, multimodal extensions) to help future maintainers track what's LoongSuite-specific vs upstream.

Automated review by github-manager-bot

@sipercai
sipercai force-pushed the feat/google-genai-observability branch from 3815b12 to 42e416e Compare August 5, 2026 06:06

@ralf0131 ralf0131 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Re-review after new commits since last approval (July 31). Latest changes include streaming output message aggregation fix and additional test coverage.

Key Changes Since Last Review

  • Streaming output aggregation (generate_content.py, message.py): Introduces StreamOutputMessageAccumulator to properly aggregate streaming deltas by candidate index. This ensures gen_ai.output.messages contains complete logical responses instead of one message per SSE chunk — a correct fix for a real data quality issue.
  • Test coverage: New test_stream_accumulator.py (135 lines) with thorough unit tests for the accumulator logic. Updated streaming base tests and fail-open tests to verify the aggregation behavior.
  • AgentScope instrumentation (_v2_middleware.py): Significant updates to v2 middleware with proper skill instrumentation and expanded test coverage (893 lines of new tests).
  • QwenPaw instrumentation (patch.py): Enhanced patch with better session tracking and turn boundary detection.
  • Multimodal upload (multimodal_upload_hook.py): Improved hook with better error handling.

All changes are well-tested and follow established patterns. The streaming aggregation fix is particularly important for data quality.

LGTM ✅


Automated review by "github-manager-bot"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants