Skip to content

Commit f39ec94

Browse files
authored
Merge branch 'main' into feature-distributed-ci-image-build
2 parents e1c4f33 + b6631c8 commit f39ec94

27 files changed

+1655
-108
lines changed

.github/workflows/tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ jobs:
100100
coverage xml
101101
102102
- name: Upload Coverage to Codecov
103-
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # 5.4.3
103+
uses: codecov/codecov-action@fdcc8476540edceab3de004e990f80d881c6cc00 # 5.5.0
104104
with:
105105
files: coverage.xml
106106
fail_ci_if_error: true
@@ -1022,7 +1022,7 @@ jobs:
10221022
- 2181:2181
10231023

10241024
kafka:
1025-
image: bitnami/kafka:3.6.1
1025+
image: bitnamilegacy/kafka:3.3.2
10261026
ports:
10271027
- 8080:8080
10281028
- 8082:8082

.github/workflows/trivy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,6 @@ jobs:
6161

6262
- name: Upload Trivy scan results to GitHub Security tab
6363
if: ${{ github.event_name == 'schedule' }}
64-
uses: github/codeql-action/upload-sarif@96f518a34f7a870018057716cc4d7a5c014bd61c # 3.29.10
64+
uses: github/codeql-action/upload-sarif@3c3833e0f8c1c83d449a7478aa59c036a9165498 # 3.29.11
6565
with:
6666
sarif_file: "trivy-results.sarif"

newrelic/api/web_transaction.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -665,13 +665,22 @@ def __init__(self, application, environ, source=None):
665665
self._request_uri = request_uri
666666

667667
if self._request_uri is not None:
668-
# Need to make sure we drop off any query string
669-
# arguments on the path if we have to fallback
670-
# to using the original REQUEST_URI. Can't use
671-
# attribute access on result as only support for
672-
# Python 2.5+.
673-
674-
self._request_uri = urlparse.urlparse(self._request_uri)[2]
668+
# This try/except logic is to handle cases where
669+
# `REQUEST_URI` is malformed or contains invalid
670+
# characters. This can happen at this point if
671+
# a malformed request is passed in and for versions
672+
# of `urllib` in the Python standard library older
673+
# than what has been released Jan 31, 2025.
674+
try:
675+
# Need to make sure we drop off any query string
676+
# arguments on the path if we have to fallback
677+
# to using the original REQUEST_URI. Can't use
678+
# attribute access on result as only support for
679+
# Python 2.5+.
680+
self._request_uri = urlparse.urlparse(self._request_uri)[2]
681+
except:
682+
# If `self._request_uri` is invalid, set to `None`
683+
self._request_uri = None
675684

676685
if script_name is not None or path_info is not None:
677686
if path_info is None:

newrelic/config.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,10 @@ def _map_inc_excl_attributes(s):
215215
return newrelic.core.config._parse_attributes(s)
216216

217217

218+
def _map_inc_excl_middleware(s):
219+
return newrelic.core.config._parse_attributes(s, middleware=True)
220+
221+
218222
def _map_case_insensitive_excl_labels(s):
219223
return [v.lower() for v in newrelic.core.config._parse_attributes(s)]
220224

@@ -510,6 +514,9 @@ def _process_configuration(section):
510514
section, "instrumentation.kombu.ignored_exchanges", "get", newrelic.core.config.parse_space_separated_into_list
511515
)
512516
_process_setting(section, "instrumentation.kombu.consumer.enabled", "getboolean", None)
517+
_process_setting(section, "instrumentation.middleware.django.enabled", "getboolean", None)
518+
_process_setting(section, "instrumentation.middleware.django.exclude", "get", _map_inc_excl_middleware)
519+
_process_setting(section, "instrumentation.middleware.django.include", "get", _map_inc_excl_middleware)
513520

514521

515522
# Loading of configuration from specified file and for specified
@@ -2700,6 +2707,10 @@ def _process_module_builtin_defaults():
27002707
"graphene.types.schema", "newrelic.hooks.framework_graphene", "instrument_graphene_types_schema"
27012708
)
27022709

2710+
_process_module_definition(
2711+
"graphene_django.views", "newrelic.hooks.component_graphenedjango", "instrument_graphene_django_views"
2712+
)
2713+
27032714
_process_module_definition("graphql.graphql", "newrelic.hooks.framework_graphql", "instrument_graphql")
27042715
_process_module_definition(
27052716
"graphql.execution.execute", "newrelic.hooks.framework_graphql", "instrument_graphql_execute"

newrelic/core/config.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,14 @@ class SpanEventAttributesSettings(Settings):
320320
pass
321321

322322

323+
class InstrumentationMiddlewareSettings(Settings):
324+
pass
325+
326+
327+
class InstrumentationDjangoMiddlewareSettings(Settings):
328+
pass
329+
330+
323331
class DistributedTracingSettings(Settings):
324332
pass
325333

@@ -507,6 +515,8 @@ class EventHarvestConfigHarvestLimitSettings(Settings):
507515
_settings.instrumentation.kombu = InstrumentationKombuSettings()
508516
_settings.instrumentation.kombu.ignored_exchanges = InstrumentationKombuIgnoredExchangesSettings()
509517
_settings.instrumentation.kombu.consumer = InstrumentationKombuConsumerSettings()
518+
_settings.instrumentation.middleware = InstrumentationMiddlewareSettings()
519+
_settings.instrumentation.middleware.django = InstrumentationDjangoMiddlewareSettings()
510520
_settings.message_tracer = MessageTracerSettings()
511521
_settings.process_host = ProcessHostSettings()
512522
_settings.rum = RumSettings()
@@ -634,11 +644,15 @@ def _parse_status_codes(value, target):
634644
return target
635645

636646

637-
def _parse_attributes(s):
647+
# Called from newrelic.config.py to parse
648+
# attributes and django middleware lists
649+
def _parse_attributes(s, middleware=False):
638650
valid = []
639651
for item in s.split():
640652
if "*" not in item[:-1] and len(item.encode("utf-8")) < 256:
641653
valid.append(item)
654+
elif middleware:
655+
_logger.warning("Improperly formatted middleware: %r", item)
642656
else:
643657
_logger.warning("Improperly formatted attribute: %r", item)
644658
return valid
@@ -904,6 +918,12 @@ def default_otlp_host(host):
904918
"NEW_RELIC_INSTRUMENTATION_KOMBU_CONSUMER_ENABLED", default=False
905919
)
906920

921+
_settings.instrumentation.middleware.django.enabled = _environ_as_bool(
922+
"NEW_RELIC_INSTRUMENTATION_DJANGO_MIDDLEWARE_ENABLED", default=True
923+
)
924+
_settings.instrumentation.middleware.django.exclude = []
925+
_settings.instrumentation.middleware.django.include = []
926+
907927
_settings.event_harvest_config.harvest_limits.analytic_event_data = _environ_as_int(
908928
"NEW_RELIC_ANALYTICS_EVENTS_MAX_SAMPLES_STORED", DEFAULT_RESERVOIR_SIZE
909929
)
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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 functools
16+
import sys
17+
from inspect import isawaitable
18+
19+
from newrelic.api.error_trace import ErrorTrace
20+
from newrelic.api.graphql_trace import GraphQLOperationTrace
21+
from newrelic.api.transaction import current_transaction
22+
from newrelic.common.object_names import callable_name
23+
from newrelic.common.object_wrapper import wrap_function_wrapper
24+
from newrelic.common.package_version_utils import get_package_version
25+
from newrelic.common.signature import bind_args
26+
from newrelic.core.graphql_utils import graphql_statement
27+
from newrelic.hooks.framework_graphql import GRAPHQL_VERSION, ignore_graphql_duplicate_exception
28+
29+
GRAPHENE_DJANGO_VERSION = get_package_version("graphene_django")
30+
graphene_django_version_tuple = tuple(map(int, GRAPHENE_DJANGO_VERSION.split(".")))
31+
32+
33+
# Implementation from `newrelic.hooks.framework_graphql_py3`
34+
def nr_coro_execute_graphql_request_wrapper(wrapped, trace, ignore, result):
35+
@functools.wraps(wrapped)
36+
async def _nr_coro_execute_graphql_request_wrapper():
37+
try:
38+
with ErrorTrace(ignore=ignore):
39+
result_ = await result
40+
except:
41+
trace.__exit__(*sys.exc_info())
42+
raise
43+
else:
44+
trace.__exit__(None, None, None)
45+
return result_
46+
47+
return _nr_coro_execute_graphql_request_wrapper()
48+
49+
50+
def wrap_GraphQLView_execute_graphql_request(wrapped, instance, args, kwargs):
51+
transaction = current_transaction()
52+
53+
if not transaction:
54+
return wrapped(*args, **kwargs)
55+
56+
transaction.add_framework_info(name="GraphQL", version=GRAPHQL_VERSION)
57+
transaction.add_framework_info(name="GrapheneDjango", version=GRAPHENE_DJANGO_VERSION)
58+
bound_args = bind_args(wrapped, args, kwargs)
59+
60+
try:
61+
schema = instance.schema.graphql_schema
62+
query = bound_args.get("query", None)
63+
except TypeError:
64+
return wrapped(*args, **kwargs)
65+
66+
transaction.set_transaction_name(callable_name(wrapped), "GraphQL", priority=11)
67+
68+
trace = GraphQLOperationTrace()
69+
70+
trace.statement = graphql_statement(query)
71+
trace.product = "GrapheneDjango"
72+
73+
# Handle Schemas created from frameworks
74+
if hasattr(schema, "_nr_framework"):
75+
framework = schema._nr_framework
76+
transaction.add_framework_info(name=framework[0], version=framework[1])
77+
78+
# Trace must be manually started and stopped to ensure it exists prior to and during the entire duration of the query.
79+
# Otherwise subsequent instrumentation will not be able to find an operation trace and will have issues.
80+
trace.__enter__()
81+
try:
82+
with ErrorTrace(ignore=ignore_graphql_duplicate_exception):
83+
result = wrapped(*args, **kwargs)
84+
except Exception:
85+
# Execution finished synchronously, exit immediately.
86+
trace.__exit__(*sys.exc_info())
87+
raise
88+
else:
89+
trace.set_transaction_name(priority=14)
90+
if isawaitable(result):
91+
# Asynchronous implementations
92+
# Return a coroutine that handles closing the operation trace
93+
return nr_coro_execute_graphql_request_wrapper(wrapped, trace, ignore_graphql_duplicate_exception, result)
94+
else:
95+
# Execution finished synchronously, exit immediately.
96+
trace.__exit__(None, None, None)
97+
return result
98+
99+
100+
def instrument_graphene_django_views(module):
101+
if hasattr(module, "GraphQLView") and hasattr(module.GraphQLView, "execute_graphql_request"):
102+
wrap_function_wrapper(module, "GraphQLView.execute_graphql_request", wrap_GraphQLView_execute_graphql_request)

0 commit comments

Comments
 (0)