Skip to content

AutoTracingPlugin._rebind drops the staticmethod descriptor, breaking wrapped classes #6819

Description

@DVDH-000

Summary

AutoTracingPlugin._wrap_module discovers class members with inspect.getmembers, which returns the underlying plain function for a @staticmethod. _rebind then setattrs a plain wrapper function back onto the class without re-wrapping it in staticmethod(...).

After that, accessing the attribute through an instance re-engages the descriptor protocol and binds the instance as the first positional argument. Any static method whose first parameter is a real parameter now receives self in that slot.

plugins/auto_tracing_plugin.py (2.7.0), _wrap_module around L145 and _rebind around L164:

for member_name, member in inspect.getmembers(attr):
    ...
    if not inspect.isfunction(member):
        continue
    ...
    self._rebind(attr, member_name, member)

# _rebind
setattr(owner, name, auto_tracing_helpers.build_tracing_wrapper(fn, self._tracer, self._caps))

Reproduction

import inspect

class C:
    @staticmethod
    def static(a, b=None):
        return ("static", a, b)

# what _wrap_module sees:
member = dict(inspect.getmembers(C))["static"]
assert inspect.isfunction(member)        # True -- passes the filter

# what _rebind does:
def make_wrapper(fn):
    def wrapper(*a, **kw):
        return fn(*a, **kw)
    return wrapper

setattr(C, "static", make_wrapper(member))

C().static(a=1)
# TypeError: C.static() got multiple values for argument 'a'

Observed in the wild against google.genai.Client._get_api_client, which is a @staticmethod whose first parameter happens to be named vertexai:

TypeError: Client._get_api_client() got multiple values for argument 'vertexai'

The instance lands in vertexai positionally and collides with the explicit vertexai= keyword at the call site.

Scope, checked rather than assumed

member kind getmembers returns isfunction reaches _rebind
staticmethod plain function True yes, corrupted
classmethod bound method False no, skipped
ordinary method plain function True yes, correct (binding self is right)

So classmethod escapes by accident, via the isfunction filter. Only staticmethod is affected.

Suggested fix

Determine the descriptor kind from the class __dict__ rather than the getmembers result, and re-wrap before setattr:

raw = owner.__dict__.get(name) if isinstance(owner, type) else None
wrapper = auto_tracing_helpers.build_tracing_wrapper(fn, self._tracer, self._caps)
if isinstance(raw, staticmethod):
    wrapper = staticmethod(wrapper)
setattr(owner, name, wrapper)

inspect.getattr_static would work too. Happy to open a PR if that would help.

Related: _add_agent_scope cannot be constrained to first-party modules

Separate but compounding. _add_agent_scope walks objects reachable from the agent to depth 30 and adds any module prefix it finds to the plugin scope. Passing extra_scope_prefixes=("my_package",) still resulted in google.genai.client being instrumented, because something in the agent object graph resolved to it.

Combined with the bug above, enabling the plugin instruments arbitrary third-party modules and then corrupts any staticmethod it finds there. In one short trial that broke two unrelated call paths.

Would a strict allowlist mode for _add_agent_scope be something you would consider?

Worth saying

The plugin does work where this does not bite. Spans on ordinary functions carried genuinely useful adk.fn.arg.* and adk.fn.return detail with no hand instrumentation, which is exactly what we were evaluating it for. These two issues are what stopped us adopting it, not the idea.

Version: google-adk 2.7.0, Python 3.13.

Metadata

Metadata

Labels

request clarification[Status] The maintainer need clarification or more information from the authortracing[Component] This issue is related to OpenTelemetry tracing

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions