Skip to content

Commit 1efa4a5

Browse files
Add subcomponent attributes to agentic AI framework instrumentations (#1666)
* Add Strands subcomponent attrs. * Update tests. * Cleanup. * Add subcomponent attrs. * Add validator to tool tests. * Cleanup tests. * Add subcomponent attribute to MCP instrumentation. * [MegaLinter] Apply linters fixes * Initial commit. * Add attrs to spans. * Update validators in tests. * Swap out subcomponent attribute names. * Update tests. * [MegaLinter] Apply linters fixes * Revert "Add subcomponent attribute to Autogen instrumentation. " --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 3f0d83b commit 1efa4a5

File tree

11 files changed

+125
-9
lines changed

11 files changed

+125
-9
lines changed

newrelic/core/attribute.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@
100100
"response.headers.contentType",
101101
"response.status",
102102
"server.address",
103+
"subcomponent",
103104
"zeebe.client.bpmnProcessId",
104105
"zeebe.client.messageName",
105106
"zeebe.client.correlationKey",

newrelic/hooks/adapter_mcp.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import json
1516
import logging
1617

1718
from newrelic.api.function_trace import FunctionTrace
@@ -37,8 +38,10 @@ async def wrap_call_tool(wrapped, instance, args, kwargs):
3738
bound_args = bind_args(wrapped, args, kwargs)
3839
tool_name = bound_args.get("name") or "tool"
3940
function_trace_name = f"{func_name}/{tool_name}"
41+
agentic_subcomponent_data = {"type": "APM-AI_TOOL", "name": tool_name}
4042

41-
with FunctionTrace(name=function_trace_name, group="Llm/tool/MCP", source=wrapped):
43+
with FunctionTrace(name=function_trace_name, group="Llm/tool/MCP", source=wrapped) as ft:
44+
ft._add_agent_attribute("subcomponent", json.dumps(agentic_subcomponent_data))
4245
return await wrapped(*args, **kwargs)
4346

4447

newrelic/hooks/mlmodel_langchain.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import json
1516
import logging
1617
import sys
1718
import time
@@ -161,9 +162,11 @@ def invoke(self, *args, **kwargs):
161162
agent_id = str(uuid.uuid4())
162163
agent_event_dict = _construct_base_agent_event_dict(agent_name, agent_id, transaction)
163164
function_trace_name = f"invoke/{agent_name}"
165+
agentic_subcomponent_data = {"type": "APM-AI_AGENT", "name": agent_name}
164166

165167
ft = FunctionTrace(name=function_trace_name, group="Llm/agent/LangChain")
166168
ft.__enter__()
169+
ft._add_agent_attribute("subcomponent", json.dumps(agentic_subcomponent_data))
167170
try:
168171
return_val = self.__wrapped__.invoke(*args, **kwargs)
169172
except Exception:
@@ -189,9 +192,11 @@ async def ainvoke(self, *args, **kwargs):
189192
agent_id = str(uuid.uuid4())
190193
agent_event_dict = _construct_base_agent_event_dict(agent_name, agent_id, transaction)
191194
function_trace_name = f"ainvoke/{agent_name}"
195+
agentic_subcomponent_data = {"type": "APM-AI_AGENT", "name": agent_name}
192196

193197
ft = FunctionTrace(name=function_trace_name, group="Llm/agent/LangChain")
194198
ft.__enter__()
199+
ft._add_agent_attribute("subcomponent", json.dumps(agentic_subcomponent_data))
195200
try:
196201
return_val = await self.__wrapped__.ainvoke(*args, **kwargs)
197202
except Exception:
@@ -217,9 +222,11 @@ def stream(self, *args, **kwargs):
217222
agent_id = str(uuid.uuid4())
218223
agent_event_dict = _construct_base_agent_event_dict(agent_name, agent_id, transaction)
219224
function_trace_name = f"stream/{agent_name}"
225+
agentic_subcomponent_data = {"type": "APM-AI_AGENT", "name": agent_name}
220226

221227
ft = FunctionTrace(name=function_trace_name, group="Llm/agent/LangChain")
222228
ft.__enter__()
229+
ft._add_agent_attribute("subcomponent", json.dumps(agentic_subcomponent_data))
223230
try:
224231
return_val = self.__wrapped__.stream(*args, **kwargs)
225232
return_val = GeneratorProxy(
@@ -242,9 +249,11 @@ def astream(self, *args, **kwargs):
242249
agent_id = str(uuid.uuid4())
243250
agent_event_dict = _construct_base_agent_event_dict(agent_name, agent_id, transaction)
244251
function_trace_name = f"astream/{agent_name}"
252+
agentic_subcomponent_data = {"type": "APM-AI_AGENT", "name": agent_name}
245253

246254
ft = FunctionTrace(name=function_trace_name, group="Llm/agent/LangChain")
247255
ft.__enter__()
256+
ft._add_agent_attribute("subcomponent", json.dumps(agentic_subcomponent_data))
248257
try:
249258
return_val = self.__wrapped__.astream(*args, **kwargs)
250259
return_val = AsyncGeneratorProxy(
@@ -267,9 +276,11 @@ def transform(self, *args, **kwargs):
267276
agent_id = str(uuid.uuid4())
268277
agent_event_dict = _construct_base_agent_event_dict(agent_name, agent_id, transaction)
269278
function_trace_name = f"stream/{agent_name}"
279+
agentic_subcomponent_data = {"type": "APM-AI_AGENT", "name": agent_name}
270280

271281
ft = FunctionTrace(name=function_trace_name, group="Llm/agent/LangChain")
272282
ft.__enter__()
283+
ft._add_agent_attribute("subcomponent", json.dumps(agentic_subcomponent_data))
273284
try:
274285
return_val = self.__wrapped__.transform(*args, **kwargs)
275286
return_val = GeneratorProxy(
@@ -292,9 +303,11 @@ def atransform(self, *args, **kwargs):
292303
agent_id = str(uuid.uuid4())
293304
agent_event_dict = _construct_base_agent_event_dict(agent_name, agent_id, transaction)
294305
function_trace_name = f"astream/{agent_name}"
306+
agentic_subcomponent_data = {"type": "APM-AI_AGENT", "name": agent_name}
295307

296308
ft = FunctionTrace(name=function_trace_name, group="Llm/agent/LangChain")
297309
ft.__enter__()
310+
ft._add_agent_attribute("subcomponent", json.dumps(agentic_subcomponent_data))
298311
try:
299312
return_val = self.__wrapped__.atransform(*args, **kwargs)
300313
return_val = AsyncGeneratorProxy(
@@ -512,8 +525,11 @@ def wrap_tool_sync_run(wrapped, instance, args, kwargs):
512525
except Exception:
513526
filtered_tool_input = tool_input
514527

528+
agentic_subcomponent_data = {"type": "APM-AI_TOOL", "name": tool_name}
529+
515530
ft = FunctionTrace(name=f"{wrapped.__name__}/{tool_name}", group="Llm/tool/LangChain")
516531
ft.__enter__()
532+
ft._add_agent_attribute("subcomponent", json.dumps(agentic_subcomponent_data))
517533
linking_metadata = get_trace_linking_metadata()
518534
try:
519535
return_val = wrapped(**run_args)
@@ -573,8 +589,11 @@ async def wrap_tool_async_run(wrapped, instance, args, kwargs):
573589
except Exception:
574590
filtered_tool_input = tool_input
575591

592+
agentic_subcomponent_data = {"type": "APM-AI_TOOL", "name": tool_name}
593+
576594
ft = FunctionTrace(name=f"{wrapped.__name__}/{tool_name}", group="Llm/tool/LangChain")
577595
ft.__enter__()
596+
ft._add_agent_attribute("subcomponent", json.dumps(agentic_subcomponent_data))
578597
linking_metadata = get_trace_linking_metadata()
579598
try:
580599
return_val = await wrapped(**run_args)

newrelic/hooks/mlmodel_strands.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import json
1516
import logging
1617
import sys
1718
import uuid
@@ -94,9 +95,12 @@ def wrap_stream_async(wrapped, instance, args, kwargs):
9495
func_name = callable_name(wrapped)
9596
agent_name = getattr(instance, "name", "agent")
9697
function_trace_name = f"{func_name}/{agent_name}"
98+
agentic_subcomponent_data = {"type": "APM-AI_AGENT", "name": agent_name}
9799

98100
ft = FunctionTrace(name=function_trace_name, group="Llm/agent/Strands")
99101
ft.__enter__()
102+
ft._add_agent_attribute("subcomponent", json.dumps(agentic_subcomponent_data))
103+
100104
linking_metadata = get_trace_linking_metadata()
101105
agent_id = str(uuid.uuid4())
102106

@@ -105,7 +109,6 @@ def wrap_stream_async(wrapped, instance, args, kwargs):
105109
except Exception:
106110
raise
107111

108-
# For streaming responses, wrap with proxy and attach metadata
109112
try:
110113
# For streaming responses, wrap with proxy and attach metadata
111114
proxied_return_val = AsyncGeneratorProxy(
@@ -126,7 +129,6 @@ def _record_agent_event_on_stop_iteration(self, transaction):
126129
# Use saved linking metadata to maintain correct span association
127130
linking_metadata = self._nr_metadata or get_trace_linking_metadata()
128131
self._nr_ft.__exit__(None, None, None)
129-
130132
try:
131133
strands_attrs = getattr(self, "_nr_strands_attrs", {})
132134

@@ -352,9 +354,11 @@ def wrap_tool_executor__stream(wrapped, instance, args, kwargs):
352354

353355
func_name = callable_name(wrapped)
354356
function_trace_name = f"{func_name}/{tool_name}"
357+
agentic_subcomponent_data = {"type": "APM-AI_TOOL", "name": tool_name}
355358

356359
ft = FunctionTrace(name=function_trace_name, group="Llm/tool/Strands")
357360
ft.__enter__()
361+
ft._add_agent_attribute("subcomponent", json.dumps(agentic_subcomponent_data))
358362
linking_metadata = get_trace_linking_metadata()
359363
tool_id = str(uuid.uuid4())
360364

tests/adapter_mcp/test_mcp.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from mcp.server.fastmcp.tools import ToolManager
2020
from testing_support.ml_testing_utils import disabled_ai_monitoring_settings
2121
from testing_support.validators.validate_function_not_called import validate_function_not_called
22+
from testing_support.validators.validate_span_events import validate_span_events
2223
from testing_support.validators.validate_transaction_metrics import validate_transaction_metrics
2324

2425
from newrelic.api.background_task import background_task
@@ -57,6 +58,7 @@ def echo_prompt(message: str):
5758
rollup_metrics=[("Llm/tool/MCP/mcp.client.session:ClientSession.call_tool/add_exclamation", 1)],
5859
background_task=True,
5960
)
61+
@validate_span_events(count=1, exact_agents={"subcomponent": '{"type": "APM-AI_TOOL", "name": "add_exclamation"}'})
6062
@background_task()
6163
def test_tool_tracing_via_client_session(loop, fastmcp_server):
6264
async def _test():
@@ -75,6 +77,7 @@ async def _test():
7577
rollup_metrics=[("Llm/tool/MCP/mcp.server.fastmcp.tools.tool_manager:ToolManager.call_tool/add_exclamation", 1)],
7678
background_task=True,
7779
)
80+
@validate_span_events(count=1, exact_agents={"subcomponent": '{"type": "APM-AI_TOOL", "name": "add_exclamation"}'})
7881
@background_task()
7982
def test_tool_tracing_via_tool_manager(loop):
8083
async def _test():

tests/mlmodel_langchain/test_agents.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import pytest
1616
from langchain.messages import HumanMessage
1717
from langchain.tools import tool
18-
from testing_support.fixtures import reset_core_stats_engine, validate_attributes
18+
from testing_support.fixtures import dt_enabled, reset_core_stats_engine, validate_attributes
1919
from testing_support.ml_testing_utils import (
2020
disabled_ai_monitoring_record_content_settings,
2121
disabled_ai_monitoring_settings,
@@ -24,6 +24,7 @@
2424
from testing_support.validators.validate_custom_event import validate_custom_event_count
2525
from testing_support.validators.validate_custom_events import validate_custom_events
2626
from testing_support.validators.validate_error_trace_attributes import validate_error_trace_attributes
27+
from testing_support.validators.validate_span_events import validate_span_events
2728
from testing_support.validators.validate_transaction_error_event_count import validate_transaction_error_event_count
2829
from testing_support.validators.validate_transaction_metrics import validate_transaction_metrics
2930

@@ -76,6 +77,7 @@ def add_exclamation(message: str) -> str:
7677
return f"{message}!"
7778

7879

80+
@dt_enabled
7981
@reset_core_stats_engine()
8082
def test_agent(exercise_agent, create_agent_runnable, set_trace_info, method_name):
8183
@validate_custom_events(events_with_context_attrs(agent_recorded_event))
@@ -87,6 +89,8 @@ def test_agent(exercise_agent, create_agent_runnable, set_trace_info, method_nam
8789
background_task=True,
8890
)
8991
@validate_attributes("agent", ["llm"])
92+
@validate_span_events(count=1, exact_agents={"subcomponent": '{"type": "APM-AI_AGENT", "name": "my_agent"}'})
93+
@validate_span_events(count=1, exact_agents={"subcomponent": '{"type": "APM-AI_TOOL", "name": "add_exclamation"}'})
9094
@background_task(name="test_agent")
9195
def _test():
9296
set_trace_info()
@@ -100,6 +104,7 @@ def _test():
100104
_test()
101105

102106

107+
@dt_enabled
103108
@reset_core_stats_engine()
104109
@disabled_ai_monitoring_record_content_settings
105110
def test_agent_no_content(exercise_agent, create_agent_runnable, set_trace_info, method_name):
@@ -112,6 +117,8 @@ def test_agent_no_content(exercise_agent, create_agent_runnable, set_trace_info,
112117
background_task=True,
113118
)
114119
@validate_attributes("agent", ["llm"])
120+
@validate_span_events(count=1, exact_agents={"subcomponent": '{"type": "APM-AI_AGENT", "name": "my_agent"}'})
121+
@validate_span_events(count=1, exact_agents={"subcomponent": '{"type": "APM-AI_TOOL", "name": "add_exclamation"}'})
115122
@background_task(name="test_agent_no_content")
116123
def _test():
117124
set_trace_info()
@@ -123,13 +130,15 @@ def _test():
123130
_test()
124131

125132

133+
@dt_enabled
126134
@reset_core_stats_engine()
127135
@validate_custom_event_count(count=0)
128136
def test_agent_outside_txn(exercise_agent, create_agent_runnable):
129137
my_agent = create_agent_runnable(tools=[add_exclamation], system_prompt="You are a text manipulation algorithm.")
130138
exercise_agent(my_agent, PROMPT)
131139

132140

141+
@dt_enabled
133142
@disabled_ai_monitoring_settings
134143
@reset_core_stats_engine()
135144
@validate_custom_event_count(count=0)
@@ -140,6 +149,7 @@ def test_agent_disabled_ai_monitoring_events(exercise_agent, create_agent_runnab
140149
exercise_agent(my_agent, PROMPT)
141150

142151

152+
@dt_enabled
143153
@reset_core_stats_engine()
144154
def test_agent_execution_error(exercise_agent, create_agent_runnable, set_trace_info, method_name, agent_runnable_type):
145155
# Add a wrapper to intentionally force an error in the Agent code
@@ -159,6 +169,8 @@ def inject_exception(wrapped, instance, args, kwargs):
159169
background_task=True,
160170
)
161171
@validate_attributes("agent", ["llm"])
172+
# Only an agent span is expected here and not a tool because the error is injected before the tool is called
173+
@validate_span_events(count=1, exact_agents={"subcomponent": '{"type": "APM-AI_AGENT", "name": "my_agent"}'})
162174
@background_task(name="test_agent_execution_error")
163175
def _test():
164176
set_trace_info()

tests/mlmodel_langchain/test_tools.py

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

1515
import pytest
1616
from langchain.messages import HumanMessage
17-
from testing_support.fixtures import reset_core_stats_engine, validate_attributes
17+
from testing_support.fixtures import dt_enabled, reset_core_stats_engine, validate_attributes
1818
from testing_support.ml_testing_utils import (
1919
disabled_ai_monitoring_record_content_settings,
2020
events_with_context_attrs,
@@ -23,6 +23,7 @@
2323
from testing_support.validators.validate_custom_event import validate_custom_event_count
2424
from testing_support.validators.validate_custom_events import validate_custom_events
2525
from testing_support.validators.validate_error_trace_attributes import validate_error_trace_attributes
26+
from testing_support.validators.validate_span_events import validate_span_events
2627
from testing_support.validators.validate_transaction_error_event_count import validate_transaction_error_event_count
2728
from testing_support.validators.validate_transaction_metrics import validate_transaction_metrics
2829

@@ -95,6 +96,7 @@
9596
]
9697

9798

99+
@dt_enabled
98100
@reset_core_stats_engine()
99101
def test_tool(exercise_agent, set_trace_info, create_agent_runnable, add_exclamation, tool_method_name):
100102
@validate_custom_events(events_with_context_attrs(tool_recorded_event))
@@ -106,6 +108,8 @@ def test_tool(exercise_agent, set_trace_info, create_agent_runnable, add_exclama
106108
background_task=True,
107109
)
108110
@validate_attributes("agent", ["llm"])
111+
@validate_span_events(count=1, exact_agents={"subcomponent": '{"type": "APM-AI_AGENT", "name": "my_agent"}'})
112+
@validate_span_events(count=1, exact_agents={"subcomponent": '{"type": "APM-AI_TOOL", "name": "add_exclamation"}'})
109113
@background_task(name="test_tool")
110114
def _test():
111115
set_trace_info()
@@ -119,6 +123,7 @@ def _test():
119123
_test()
120124

121125

126+
@dt_enabled
122127
@reset_core_stats_engine()
123128
@disabled_ai_monitoring_record_content_settings
124129
def test_tool_no_content(exercise_agent, set_trace_info, create_agent_runnable, add_exclamation, tool_method_name):
@@ -131,6 +136,8 @@ def test_tool_no_content(exercise_agent, set_trace_info, create_agent_runnable,
131136
background_task=True,
132137
)
133138
@validate_attributes("agent", ["llm"])
139+
@validate_span_events(count=1, exact_agents={"subcomponent": '{"type": "APM-AI_AGENT", "name": "my_agent"}'})
140+
@validate_span_events(count=1, exact_agents={"subcomponent": '{"type": "APM-AI_TOOL", "name": "add_exclamation"}'})
134141
@background_task(name="test_tool_no_content")
135142
def _test():
136143
set_trace_info()
@@ -142,6 +149,7 @@ def _test():
142149
_test()
143150

144151

152+
@dt_enabled
145153
@reset_core_stats_engine()
146154
def test_tool_execution_error(exercise_agent, set_trace_info, create_agent_runnable, add_exclamation, tool_method_name):
147155
@validate_transaction_error_event_count(1)
@@ -157,6 +165,8 @@ def test_tool_execution_error(exercise_agent, set_trace_info, create_agent_runna
157165
background_task=True,
158166
)
159167
@validate_attributes("agent", ["llm"])
168+
@validate_span_events(count=1, exact_agents={"subcomponent": '{"type": "APM-AI_AGENT", "name": "my_agent"}'})
169+
@validate_span_events(count=1, exact_agents={"subcomponent": '{"type": "APM-AI_TOOL", "name": "add_exclamation"}'})
160170
@background_task(name="test_tool_execution_error")
161171
def _test():
162172
set_trace_info()
@@ -169,6 +179,7 @@ def _test():
169179
_test()
170180

171181

182+
@dt_enabled
172183
@reset_core_stats_engine()
173184
def test_tool_pre_execution_exception(
174185
exercise_agent, set_trace_info, create_agent_runnable, add_exclamation, tool_method_name
@@ -190,6 +201,8 @@ def inject_exception(wrapped, instance, args, kwargs):
190201
background_task=True,
191202
)
192203
@validate_attributes("agent", ["llm"])
204+
@validate_span_events(count=1, exact_agents={"subcomponent": '{"type": "APM-AI_AGENT", "name": "my_agent"}'})
205+
@validate_span_events(count=1, exact_agents={"subcomponent": '{"type": "APM-AI_TOOL", "name": "add_exclamation"}'})
193206
@background_task(name="test_tool_pre_execution_exception")
194207
def _test():
195208
set_trace_info()

0 commit comments

Comments
 (0)