[google-genai] uninstrument() raises AssertionError because __init__ resets the method snapshot on the singleton instrumentor; calls stay traced

Closed Beginner friendly
#845 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

A pull request for this has already been merged.

  • #853 by @VimalN2005 — merged

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
74/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Active
Tech stack
python

Research direction

Start with GoogleGenAiSdkInstrumentor.init and follow the snapshot lifecycle through _instrument() and _uninstrument(). Reproduce the issue with the supplied script, including constructing the singleton after instrumenting. Done means uninstrument() returns normally, later generate_content calls produce zero spans, and is_instrumented_by_opentelemetry is False.

Written by the indexing model from the issue text.

Description

Describe your environment
  • opentelemetry-instrumentation-google-genai 1.2b0
  • google-genai 2.28.0
  • Python 3.x, Linux
What happened?

GoogleGenAiSdkInstrumentor().uninstrument() raises AssertionError and does not remove the instrumentation. Later generate_content calls are still traced, and is_instrumented_by_opentelemetry stays True, so the instrumentor cannot be re-instrumented either.

BaseInstrumentor is a singleton (__new__ returns the existing instance), but Python still runs __init__ on every GoogleGenAiSdkInstrumentor() call. GoogleGenAiSdkInstrumentor.__init__ sets self._generate_content_snapshot = None (and the interactions/embeddings snapshots). Any later construction therefore wipes the snapshots saved by _instrument(), and _uninstrument() then hits assert isinstance(snapshot, _MethodsSnapshot) in uninstrument_generate_content.

This also breaks code that keeps the original instance: constructing GoogleGenAiSdkInstrumentor() anywhere after instrument() (for example to check is_instrumented_by_opentelemetry) wipes the snapshot, so inst.uninstrument() fails too:

inst = GoogleGenAiSdkInstrumentor()
inst.instrument()
GoogleGenAiSdkInstrumentor()   # same object, __init__ resets the snapshot
inst.uninstrument()            # AssertionError
Steps to Reproduce
import logging, traceback
logging.disable(logging.WARNING)

import httpx
from google import genai
from google.genai import types
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.instrumentation.google_genai import GoogleGenAiSdkInstrumentor

BODY = {"candidates": [{"content": {"role": "model", "parts": [{"text": "hello"}]},
                        "finishReason": "STOP"}]}
client = genai.Client(api_key="x", http_options=types.HttpOptions(
    base_url="http://mock",
    httpx_client=httpx.Client(transport=httpx.MockTransport(lambda r: httpx.Response(200, json=BODY)))))

exporter = InMemorySpanExporter()
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(exporter))

def call(label):
    exporter.clear()
    client.models.generate_content(model="m", contents="hi")
    print(f"{label:45s} spans: {len(exporter.get_finished_spans())}")

GoogleGenAiSdkInstrumentor().instrument(tracer_provider=provider)
call("after instrument()")

try:
    GoogleGenAiSdkInstrumentor().uninstrument()
    print("uninstrument() returned normally")
except Exception:
    tb = traceback.format_exc().strip().splitlines()
    print("uninstrument() raised:", tb[-1], "|", tb[-3].strip())
call("after uninstrument() (expected 0)")
print("is_instrumented_by_opentelemetry:", GoogleGenAiSdkInstrumentor().is_instrumented_by_opentelemetry)
Expected Result

uninstrument() returns normally, the call after it produces 0 spans, and is_instrumented_by_opentelemetry is False.

Actual Result
after instrument()                            spans: 1
uninstrument() raised: AssertionError | assert isinstance(snapshot, _MethodsSnapshot)
after uninstrument() (expected 0)             spans: 1
is_instrumented_by_opentelemetry: True
Additional context

Possible fix: do not reset the snapshots in __init__ when they already exist (e.g. only initialize them if the attribute is missing), or store them at module or class level. Other instrumentors in this repo that keep state in __init__ may be affected in the same way.

Dominant language
Python
Stars
48
Forks
105
Avg merge
2d 2h
Merged PRs (30d)
109

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from open-telemetry/opentelemetry-python-genai

All issues in open-telemetry/opentelemetry-python-genai

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.