Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ef63449
LangGraph Instrumentation
TimPansino Jan 13, 2026
56f14cc
LangChain Agents Instrumentation
TimPansino Jan 13, 2026
4929f79
Update tox versions for LangChain / LangGraph
TimPansino Jan 13, 2026
b8b6cca
Move GeneratorProxy from Strands to common file
TimPansino Jan 13, 2026
71445b9
More verbose logging in validate_custom_events
TimPansino Jan 13, 2026
b9e22f7
Improve prompt logging for mock openai server
TimPansino Jan 13, 2026
4de3870
Tweak langchain test folder structure
TimPansino Jan 13, 2026
b6d1dda
Update Chain tests
TimPansino Jan 13, 2026
74fdac5
New Agent testing
TimPansino Jan 13, 2026
6486445
New Tool testing
TimPansino Jan 13, 2026
55e9af3
Expand Test Matrixing
TimPansino Jan 13, 2026
80bff13
Newly recorded responses for LangChain
TimPansino Jan 13, 2026
ac2c776
Patch incorrect super() call in GeneratorProxy
TimPansino Jan 13, 2026
0f672b2
Better entry point for agent exception testing
TimPansino Jan 13, 2026
bca09c0
Update AgentObjectProxy to include transform() methods
TimPansino Jan 14, 2026
447516f
Update event counts in RunnableSequence tests
TimPansino Jan 14, 2026
ca2d253
Reformatting to kwargs
TimPansino Jan 21, 2026
1116f51
Formatting
TimPansino Jan 21, 2026
ffa5332
Remove storage of agent name on transaction
TimPansino Jan 21, 2026
be2e1e1
Instrument RunnableSequence.stream and astream
TimPansino Jan 21, 2026
f8c0808
Add correct event counts
TimPansino Jan 21, 2026
384fe2d
Guard metadata additions
TimPansino Jan 21, 2026
1ef9735
Add alternate source for agent_name
TimPansino Jan 21, 2026
c5644df
Pin lower bound of langchain tests
TimPansino Jan 21, 2026
b86e8f1
Implement tee() and __copy__() for GeneratorProxy
TimPansino Jan 22, 2026
0d47eae
Slight tweaks
TimPansino Jan 22, 2026
5b67731
Fixups.
umaannamalai Jan 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 90 additions & 6 deletions newrelic/common/llm_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,97 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import itertools
import logging

from newrelic.api.transaction import current_transaction
from newrelic.common.object_wrapper import ObjectProxy

_logger = logging.getLogger(__name__)


def _get_llm_metadata(transaction):
# Grab LLM-related custom attributes off of the transaction to store as metadata on LLM events
custom_attrs_dict = transaction._custom_params
llm_metadata_dict = {key: value for key, value in custom_attrs_dict.items() if key.startswith("llm.")}
llm_context_attrs = getattr(transaction, "_llm_context_attrs", None)
if llm_context_attrs:
llm_metadata_dict.update(llm_context_attrs)
if not transaction:
return {}
try:
# Grab LLM-related custom attributes off of the transaction to store as metadata on LLM events
custom_attrs_dict = getattr(transaction, "_custom_params", {})
llm_metadata_dict = {key: value for key, value in custom_attrs_dict.items() if key.startswith("llm.")}
llm_context_attrs = getattr(transaction, "_llm_context_attrs", None)
if llm_context_attrs:
llm_metadata_dict.update(llm_context_attrs)
except Exception:
_logger.warning("Unable to capture custom metadata attributes to record on LLM events.")
return {}

return llm_metadata_dict


class GeneratorProxy(ObjectProxy):
def __init__(self, wrapped, on_stop_iteration, on_error):
super().__init__(wrapped)
self._nr_on_stop_iteration = on_stop_iteration
self._nr_on_error = on_error

def __iter__(self):
self._nr_wrapped_iter = self.__wrapped__.__iter__()
return self

def __next__(self):
transaction = current_transaction()
if not transaction:
return self._nr_wrapped_iter.__next__()

return_val = None
try:
return_val = self._nr_wrapped_iter.__next__()
except StopIteration:
self._nr_on_stop_iteration(self, transaction)
raise
except Exception:
self._nr_on_error(self, transaction)
raise
return return_val

def close(self):
return self.__wrapped__.close()

def __copy__(self):
# Required to properly interface with itertool.tee, which can be called by LangChain on generators
self.__wrapped__, copy = itertools.tee(self.__wrapped__, 2)
return GeneratorProxy(copy, self._nr_on_stop_iteration, self._nr_on_error)


class AsyncGeneratorProxy(ObjectProxy):
def __init__(self, wrapped, on_stop_iteration, on_error):
super().__init__(wrapped)
self._nr_on_stop_iteration = on_stop_iteration
self._nr_on_error = on_error

def __aiter__(self):
self._nr_wrapped_iter = self.__wrapped__.__aiter__()
return self

async def __anext__(self):
transaction = current_transaction()
if not transaction:
return await self._nr_wrapped_iter.__anext__()

return_val = None
try:
return_val = await self._nr_wrapped_iter.__anext__()
except StopAsyncIteration:
self._nr_on_stop_iteration(self, transaction)
raise
except Exception:
self._nr_on_error(self, transaction)
raise
return return_val

async def aclose(self):
return await self.__wrapped__.aclose()

def __copy__(self):
# Required to properly interface with itertool.tee, which can be called by LangChain on generators
self.__wrapped__, copy = itertools.tee(self.__wrapped__, n=2)
return AsyncGeneratorProxy(copy, self._nr_on_stop_iteration, self._nr_on_error)
20 changes: 13 additions & 7 deletions newrelic/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2088,6 +2088,10 @@ def _process_module_builtin_defaults():

_process_module_definition("asyncio.runners", "newrelic.hooks.coroutines_asyncio", "instrument_asyncio_runners")

_process_module_definition(
"langgraph.prebuilt.tool_node", "newrelic.hooks.mlmodel_langgraph", "instrument_langgraph_prebuilt_tool_node"
)

_process_module_definition(
"langchain_core.runnables.base",
"newrelic.hooks.mlmodel_langchain",
Expand All @@ -2099,13 +2103,19 @@ def _process_module_builtin_defaults():
"instrument_langchain_core_runnables_config",
)
_process_module_definition(
"langchain.chains.base", "newrelic.hooks.mlmodel_langchain", "instrument_langchain_chains_base"
"langchain_core.tools.structured",
"newrelic.hooks.mlmodel_langchain",
"instrument_langchain_core_tools_structured",
)

_process_module_definition(
"langchain_classic.chains.base", "newrelic.hooks.mlmodel_langchain", "instrument_langchain_chains_base"
"langchain.agents.factory", "newrelic.hooks.mlmodel_langchain", "instrument_langchain_agents_factory"
)
_process_module_definition(
"langchain_core.callbacks.manager", "newrelic.hooks.mlmodel_langchain", "instrument_langchain_callbacks_manager"
"langchain.chains.base", "newrelic.hooks.mlmodel_langchain", "instrument_langchain_chains_base"
)
_process_module_definition(
"langchain_classic.chains.base", "newrelic.hooks.mlmodel_langchain", "instrument_langchain_chains_base"
)

# VectorStores with similarity_search method
Expand Down Expand Up @@ -2671,10 +2681,6 @@ def _process_module_builtin_defaults():
"langchain_core.tools", "newrelic.hooks.mlmodel_langchain", "instrument_langchain_core_tools"
)

_process_module_definition(
"langchain_core.callbacks.manager", "newrelic.hooks.mlmodel_langchain", "instrument_langchain_callbacks_manager"
)

_process_module_definition("asgiref.sync", "newrelic.hooks.adapter_asgiref", "instrument_asgiref_sync")

_process_module_definition(
Expand Down
4 changes: 2 additions & 2 deletions newrelic/hooks/external_botocore.py
Original file line number Diff line number Diff line change
Expand Up @@ -1070,7 +1070,7 @@ def __next__(self):
return return_val

def close(self):
return super().close()
return self.__wrapped__.close()


class AsyncEventStreamWrapper(ObjectProxy):
Expand Down Expand Up @@ -1108,7 +1108,7 @@ async def __anext__(self):
return return_val

async def aclose(self):
return await super().aclose()
return await self.__wrapped__.aclose()


def handle_embedding_event(transaction, bedrock_attrs):
Expand Down
Loading
Loading