Skip to content

Commit 378260f

Browse files
committed
Google ADK Agents Instrumentation
1 parent a7b31f7 commit 378260f

7 files changed

Lines changed: 537 additions & 69 deletions

File tree

newrelic/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3185,6 +3185,9 @@ def _process_module_builtin_defaults():
31853185
"newrelic.hooks.mlmodel_autogen",
31863186
"instrument_autogen_agentchat_agents__assistant_agent",
31873187
)
3188+
_process_module_definition(
3189+
"google.adk.agents.llm_agent", "newrelic.hooks.mlmodel_googleadk", "instrument_googleadk_agents_llm_agent"
3190+
)
31883191
_process_module_definition(
31893192
"strands.agent.agent", "newrelic.hooks.mlmodel_strands", "instrument_strands_agent_agent"
31903193
)
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# Copyright 2010 New Relic, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import json
16+
import logging
17+
import sys
18+
import uuid
19+
20+
from newrelic.api.function_trace import FunctionTrace
21+
from newrelic.api.time_trace import get_trace_linking_metadata
22+
from newrelic.api.transaction import current_transaction
23+
from newrelic.common.llm_utils import AsyncLLMStreamProxy, _get_llm_metadata
24+
from newrelic.common.object_names import callable_name
25+
from newrelic.common.object_wrapper import wrap_function_wrapper
26+
from newrelic.common.package_version_utils import get_package_version
27+
from newrelic.core.config import global_settings
28+
29+
_logger = logging.getLogger(__name__)
30+
GOOGLEADK_VERSION = get_package_version("google-adk")
31+
32+
RECORD_EVENTS_FAILURE_LOG_MESSAGE = "Exception occurred in Google ADK instrumentation: Failed to record LLM events. Please report this issue to New Relic Support."
33+
AGENT_EVENT_FAILURE_LOG_MESSAGE = "Exception occurred in Google ADK instrumentation: Failed to record agent data. Please report this issue to New Relic Support."
34+
35+
36+
def wrap_llm_agent__run_async_impl(wrapped, instance, args, kwargs):
37+
transaction = current_transaction()
38+
if not transaction:
39+
return wrapped(*args, **kwargs)
40+
41+
settings = transaction.settings or global_settings()
42+
if not settings.ai_monitoring.enabled:
43+
return wrapped(*args, **kwargs)
44+
45+
transaction.add_ml_model_info("GoogleADK", GOOGLEADK_VERSION)
46+
transaction._add_agent_attribute("llm", True)
47+
48+
func_name = callable_name(wrapped)
49+
agent_name = getattr(instance, "name", "agent")
50+
function_trace_name = f"{func_name}/{agent_name}"
51+
agentic_subcomponent_data = {"type": "APM-AI_AGENT", "name": agent_name}
52+
53+
ft = FunctionTrace(name=function_trace_name, group="Llm/agent/GoogleADK")
54+
ft.__enter__()
55+
ft._add_agent_attribute("subcomponent", json.dumps(agentic_subcomponent_data))
56+
57+
linking_metadata = get_trace_linking_metadata()
58+
agent_id = str(uuid.uuid4())
59+
60+
try:
61+
return_val = wrapped(*args, **kwargs)
62+
except Exception:
63+
ft.__exit__(*sys.exc_info())
64+
raise
65+
66+
try:
67+
proxied_return_val = AsyncLLMStreamProxy(
68+
wrapped=return_val,
69+
on_stop_iteration=_record_agent_event_on_stop_iteration,
70+
on_error=_handle_agent_streaming_completion_error,
71+
)
72+
proxied_return_val._nr_ft = ft
73+
proxied_return_val._nr_metadata = linking_metadata
74+
proxied_return_val._nr_adk_attrs = {"agent_name": agent_name, "agent_id": agent_id}
75+
return proxied_return_val
76+
except Exception:
77+
ft.__exit__(*sys.exc_info())
78+
return return_val
79+
80+
81+
def _record_agent_event_on_stop_iteration(self, transaction):
82+
if hasattr(self, "_nr_ft"):
83+
linking_metadata = self._nr_metadata or get_trace_linking_metadata()
84+
self._nr_ft.__exit__(None, None, None)
85+
try:
86+
adk_attrs = getattr(self, "_nr_adk_attrs", {})
87+
if not adk_attrs:
88+
return
89+
90+
agent_name = adk_attrs.get("agent_name", "agent")
91+
agent_id = adk_attrs.get("agent_id")
92+
agent_event_dict = _construct_base_agent_event_dict(
93+
agent_name=agent_name, agent_id=agent_id, transaction=transaction, linking_metadata=linking_metadata
94+
)
95+
agent_event_dict["duration"] = self._nr_ft.duration * 1000
96+
transaction.record_custom_event("LlmAgent", agent_event_dict)
97+
except Exception:
98+
_logger.warning(RECORD_EVENTS_FAILURE_LOG_MESSAGE, exc_info=True)
99+
finally:
100+
if hasattr(self, "_nr_adk_attrs"):
101+
self._nr_adk_attrs.clear()
102+
103+
104+
def _handle_agent_streaming_completion_error(self, transaction):
105+
if hasattr(self, "_nr_ft"):
106+
adk_attrs = getattr(self, "_nr_adk_attrs", {})
107+
if not adk_attrs:
108+
self._nr_ft.__exit__(*sys.exc_info())
109+
return
110+
111+
linking_metadata = self._nr_metadata or get_trace_linking_metadata()
112+
113+
try:
114+
agent_name = adk_attrs.get("agent_name", "agent")
115+
agent_id = adk_attrs.get("agent_id")
116+
117+
self._nr_ft.notice_error(attributes={"agent_id": agent_id})
118+
self._nr_ft.__exit__(*sys.exc_info())
119+
120+
agent_event_dict = _construct_base_agent_event_dict(
121+
agent_name=agent_name, agent_id=agent_id, transaction=transaction, linking_metadata=linking_metadata
122+
)
123+
agent_event_dict.update({"duration": self._nr_ft.duration * 1000, "error": True})
124+
transaction.record_custom_event("LlmAgent", agent_event_dict)
125+
except Exception:
126+
_logger.warning(RECORD_EVENTS_FAILURE_LOG_MESSAGE, exc_info=True)
127+
finally:
128+
if hasattr(self, "_nr_adk_attrs"):
129+
self._nr_adk_attrs.clear()
130+
131+
132+
def _construct_base_agent_event_dict(agent_name, agent_id, transaction, linking_metadata):
133+
try:
134+
agent_event_dict = {
135+
"id": agent_id,
136+
"name": agent_name,
137+
"span_id": linking_metadata.get("span.id"),
138+
"trace_id": linking_metadata.get("trace.id"),
139+
"vendor": "google_adk",
140+
"ingest_source": "Python",
141+
}
142+
agent_event_dict.update(_get_llm_metadata(transaction))
143+
except Exception:
144+
_logger.warning(AGENT_EVENT_FAILURE_LOG_MESSAGE, exc_info=True)
145+
agent_event_dict = {}
146+
147+
return agent_event_dict
148+
149+
150+
def instrument_googleadk_agents_llm_agent(module):
151+
if hasattr(module, "LlmAgent") and hasattr(module.LlmAgent, "_run_async_impl"):
152+
wrap_function_wrapper(module, "LlmAgent._run_async_impl", wrap_llm_agent__run_async_impl)

tests/mlmodel_googleadk/_test_agent.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
from google.adk.agents import LlmAgent
1616

17-
# Model is hit live during recording; replay mode never reaches the network.
1817
MODEL = "gemini-3.5-flash"
1918
AGENT_NAME = "my_agent"
2019
AGENT_INSTRUCTION = "Answer the user's question in one word."
@@ -24,3 +23,35 @@
2423
def build_agent(tools=None):
2524
"""Return an LlmAgent. tools defaults to none (pure-LLM path)."""
2625
return LlmAgent(name=AGENT_NAME, model=MODEL, instruction=AGENT_INSTRUCTION, tools=tools or [])
26+
27+
28+
agent_recorded_event = [
29+
(
30+
{"type": "LlmAgent"},
31+
{
32+
"id": None,
33+
"name": AGENT_NAME,
34+
"span_id": None,
35+
"trace_id": "trace-id",
36+
"vendor": "google_adk",
37+
"ingest_source": "Python",
38+
"duration": None,
39+
},
40+
)
41+
]
42+
43+
agent_recorded_event_error = [
44+
(
45+
{"type": "LlmAgent"},
46+
{
47+
"id": None,
48+
"name": AGENT_NAME,
49+
"span_id": None,
50+
"trace_id": "trace-id",
51+
"vendor": "google_adk",
52+
"ingest_source": "Python",
53+
"duration": None,
54+
"error": True,
55+
},
56+
)
57+
]

0 commit comments

Comments
 (0)