Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
d0598f9
Bump oxsecurity/megalinter in the github_actions group (#1621)
dependabot[bot] Jan 5, 2026
854e966
Update tests and instrumentation for gpt-5.1. (#1620)
umaannamalai Jan 8, 2026
c1168fd
Add additional CVE to trivy ignore (#1624)
TimPansino Jan 9, 2026
a7ade08
Bump the github_actions group with 2 updates (#1626)
dependabot[bot] Jan 15, 2026
5f3cd80
Fix LangChain Tests (#1631)
TimPansino Jan 22, 2026
b2c7cf2
Update output message timestamping. (#1627)
umaannamalai Jan 22, 2026
d5bb817
Guard Azure Functions Utilization (#1632)
TimPansino Jan 22, 2026
a8b6bd8
Improve Strands Tool Error Capturing (#1623)
TimPansino Jan 22, 2026
002fc85
Add support for BaseException instances as arguments to notice_error …
TimPansino Jan 22, 2026
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
80f4a3c
Merge pull request #1630 from newrelic/feat-langgraph
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
1 change: 1 addition & 0 deletions .github/.trivyignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
CVE-2025-50181 # Requires misconfiguration of urllib3, which agent does not do without intervention
CVE-2025-66418 # Malicious servers could cause high resource consumption
CVE-2025-66471 # Malicious servers could cause high resource consumption
CVE-2026-21441 # Improper Handling of Highly Compressed Data (Data Amplification)

# =======================
# Ignored Vulnerabilities
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/mega-linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
# MegaLinter
- name: MegaLinter
id: ml
uses: oxsecurity/megalinter/flavors/python@55a59b24a441e0e1943080d4a512d827710d4a9d # 9.2.0
uses: oxsecurity/megalinter/flavors/python@42bb470545e359597e7f12156947c436e4e3fb9a # 9.3.0
env:
# All available variables are described in documentation
# https://megalinter.io/latest/configuration/
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ jobs:
git fetch --tags origin

- name: Install uv
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # 7.1.6
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # 7.2.0

- name: Install Python
run: |
Expand Down Expand Up @@ -370,7 +370,7 @@ jobs:
git fetch --tags origin

- name: Install uv
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # 7.1.6
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # 7.2.0

- name: Install Python
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/trivy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,6 @@ jobs:

- name: Upload Trivy scan results to GitHub Security tab
if: ${{ github.event_name == 'schedule' }}
uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # 4.31.9
uses: github/codeql-action/upload-sarif@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # 4.31.10
with:
sarif_file: "trivy-results.sarif"
16 changes: 10 additions & 6 deletions newrelic/api/time_trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,15 +362,19 @@ def _observe_exception(self, exc_info=None, ignore=None, expected=None, status_c
def notice_error(self, error=None, attributes=None, expected=None, ignore=None, status_code=None):
attributes = attributes if attributes is not None else {}

# If no exception details provided, use current exception.
# If an exception instance is passed, attempt to unpack it into an exception tuple with traceback
if isinstance(error, BaseException):
error = (type(error), error, getattr(error, "__traceback__", None))

# Pull from sys.exc_info if no exception is passed
if not error or None in error:
# Use current exception from sys.exc_info() if no exception was passed,
# or if the exception tuple is missing components like the traceback
if not error or (isinstance(error, (tuple, list)) and None in error):
error = sys.exc_info()

# If no exception to report, exit
if not error or None in error:
return
# Error should be a tuple or list of 3 elements by this point.
# If it's falsey or missing a component like the traceback, quietly exit early.
if not isinstance(error, (tuple, list)) or len(error) != 3 or None in error:
return

exc, value, tb = error

Expand Down
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)
28 changes: 18 additions & 10 deletions newrelic/common/utilization.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,21 +233,29 @@ class AzureFunctionUtilization(CommonUtilization):
HEADERS = {"Metadata": "true"} # noqa: RUF012
VENDOR_NAME = "azurefunction"

@staticmethod
def fetch():
@classmethod
def fetch(cls):
cloud_region = os.environ.get("REGION_NAME")
website_owner_name = os.environ.get("WEBSITE_OWNER_NAME")
azure_function_app_name = os.environ.get("WEBSITE_SITE_NAME")

if all((cloud_region, website_owner_name, azure_function_app_name)):
if website_owner_name.endswith("-Linux"):
resource_group_name = AZURE_RESOURCE_GROUP_NAME_RE.search(website_owner_name).group(1)
else:
resource_group_name = AZURE_RESOURCE_GROUP_NAME_PARTIAL_RE.search(website_owner_name).group(1)
subscription_id = re.search(r"(?:(?!\+).)*", website_owner_name).group(0)
faas_app_name = f"/subscriptions/{subscription_id}/resourceGroups/{resource_group_name}/providers/Microsoft.Web/sites/{azure_function_app_name}"
# Only send if all values are present
return (faas_app_name, cloud_region)
try:
if website_owner_name.endswith("-Linux"):
resource_group_name = AZURE_RESOURCE_GROUP_NAME_RE.search(website_owner_name).group(1)
else:
resource_group_name = AZURE_RESOURCE_GROUP_NAME_PARTIAL_RE.search(website_owner_name).group(1)
subscription_id = re.search(r"(?:(?!\+).)*", website_owner_name).group(0)
faas_app_name = f"/subscriptions/{subscription_id}/resourceGroups/{resource_group_name}/providers/Microsoft.Web/sites/{azure_function_app_name}"
# Only send if all values are present
return (faas_app_name, cloud_region)
except Exception:
_logger.debug(
"Unable to determine Azure Functions subscription id from WEBSITE_OWNER_NAME. %r",
website_owner_name,
)

return None

@classmethod
def get_values(cls, response):
Expand Down
23 changes: 16 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.chains.base", "newrelic.hooks.mlmodel_langchain", "instrument_langchain_chains_base"
)
_process_module_definition(
"langchain_core.callbacks.manager", "newrelic.hooks.mlmodel_langchain", "instrument_langchain_callbacks_manager"
"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 Expand Up @@ -2957,6 +2963,9 @@ def _process_module_builtin_defaults():
_process_module_definition(
"strands.multiagent.swarm", "newrelic.hooks.mlmodel_strands", "instrument_strands_multiagent_swarm"
)
_process_module_definition(
"strands.tools.decorator", "newrelic.hooks.mlmodel_strands", "instrument_strands_tools_decorator"
)
_process_module_definition(
"strands.tools.executors._executor",
"newrelic.hooks.mlmodel_strands",
Expand Down
17 changes: 11 additions & 6 deletions newrelic/core/stats_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -678,7 +678,6 @@ def record_time_metrics(self, metrics):
def notice_error(self, error=None, attributes=None, expected=None, ignore=None, status_code=None):
attributes = attributes if attributes is not None else {}
settings = self.__settings

if not settings:
return

Expand All @@ -690,13 +689,19 @@ def notice_error(self, error=None, attributes=None, expected=None, ignore=None,
if not settings.collect_errors and not settings.collect_error_events:
return

# Pull from sys.exc_info if no exception is passed
if not error or None in error:
# If an exception instance is passed, attempt to unpack it into an exception tuple with traceback
if isinstance(error, BaseException):
error = (type(error), error, getattr(error, "__traceback__", None))

# Use current exception from sys.exc_info() if no exception was passed,
# or if the exception tuple is missing components like the traceback
if not error or (isinstance(error, (tuple, list)) and None in error):
error = sys.exc_info()

# If no exception to report, exit
if not error or None in error:
return
# Error should be a tuple or list of 3 elements by this point.
# If it's falsey or missing a component like the traceback, quietly exit early.
if not isinstance(error, (tuple, list)) or len(error) != 3 or None in error:
return

exc, value, tb = error

Expand Down
6 changes: 2 additions & 4 deletions newrelic/hooks/external_botocore.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,8 +270,6 @@ def create_chat_completion_message_event(

if settings.ai_monitoring.record_content.enabled:
chat_completion_message_dict["content"] = content
if request_timestamp:
chat_completion_message_dict["timestamp"] = request_timestamp

chat_completion_message_dict.update(llm_metadata_dict)

Expand Down Expand Up @@ -1072,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 @@ -1110,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
2 changes: 0 additions & 2 deletions newrelic/hooks/mlmodel_gemini.py
Original file line number Diff line number Diff line change
Expand Up @@ -564,8 +564,6 @@ def create_chat_completion_message_event(

if settings.ai_monitoring.record_content.enabled:
chat_completion_output_message_dict["content"] = message_content
if request_timestamp:
chat_completion_output_message_dict["timestamp"] = request_timestamp

chat_completion_output_message_dict.update(llm_metadata)

Expand Down
Loading
Loading