Skip to content

Commit b75ea43

Browse files
AIOBotocore instrumentation (#1135)
* Instrument aiobotocore * Replace __version__ in flask instrumentation to avoid deprecation * Disable browser monitoring * Fix typo * Disable browser monitoring with aiobotocore * Fix linter errors * Revert to disabling settings in conftest * Remove browser monitoring disabling flag --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 20c5298 commit b75ea43

10 files changed

+697
-12
lines changed

newrelic/api/web_transaction.py

+5-6
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,7 @@
3333
)
3434
from newrelic.common.object_names import callable_name
3535
from newrelic.common.object_wrapper import FunctionWrapper, wrap_object
36-
from newrelic.core.attribute import create_attributes, process_user_attribute
37-
from newrelic.core.attribute_filter import DST_BROWSER_MONITORING, DST_NONE
36+
from newrelic.core.attribute_filter import DST_BROWSER_MONITORING
3837
from newrelic.packages import six
3938

4039
_logger = logging.getLogger(__name__)
@@ -457,15 +456,15 @@ def browser_timing_header(self, nonce=None):
457456

458457
# create the data structure that pull all our data in
459458

460-
broswer_agent_configuration = self.browser_monitoring_intrinsics(obfuscation_key)
459+
browser_agent_configuration = self.browser_monitoring_intrinsics(obfuscation_key)
461460

462461
if attributes:
463462
attributes = obfuscate(json_encode(attributes), obfuscation_key)
464-
broswer_agent_configuration["atts"] = attributes
463+
browser_agent_configuration["atts"] = attributes
465464

466465
header = _js_agent_header_fragment % (
467466
_encode_nonce(nonce),
468-
json_encode(broswer_agent_configuration),
467+
json_encode(browser_agent_configuration),
469468
self._settings.js_agent_loader,
470469
)
471470

@@ -568,7 +567,7 @@ def __iter__(self):
568567
yield "content-length", self.environ["CONTENT_LENGTH"]
569568
elif key == "CONTENT_TYPE":
570569
yield "content-type", self.environ["CONTENT_TYPE"]
571-
elif key == "HTTP_CONTENT_LENGTH" or key == "HTTP_CONTENT_TYPE":
570+
elif key in ("HTTP_CONTENT_LENGTH", "HTTP_CONTENT_TYPE"):
572571
# These keys are illegal and should be ignored
573572
continue
574573
elif key.startswith("HTTP_"):

newrelic/config.py

+6
Original file line numberDiff line numberDiff line change
@@ -4508,6 +4508,12 @@ def _process_module_builtin_defaults():
45084508
"instrument_gearman_worker",
45094509
)
45104510

4511+
_process_module_definition(
4512+
"aiobotocore.endpoint",
4513+
"newrelic.hooks.external_aiobotocore",
4514+
"instrument_aiobotocore_endpoint",
4515+
)
4516+
45114517
_process_module_definition(
45124518
"botocore.endpoint",
45134519
"newrelic.hooks.external_botocore",
+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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+
from newrelic.api.external_trace import ExternalTrace
16+
from newrelic.common.object_wrapper import wrap_function_wrapper
17+
18+
19+
def _bind_make_request_params(operation_model, request_dict, *args, **kwargs):
20+
return operation_model, request_dict
21+
22+
23+
def bind__send_request(request_dict, operation_model, *args, **kwargs):
24+
return operation_model, request_dict
25+
26+
27+
async def wrap_endpoint_make_request(wrapped, instance, args, kwargs):
28+
operation_model, request_dict = _bind_make_request_params(*args, **kwargs)
29+
url = request_dict.get("url")
30+
method = request_dict.get("method")
31+
32+
with ExternalTrace(library="aiobotocore", url=url, method=method, source=wrapped) as trace:
33+
try:
34+
trace._add_agent_attribute("aws.operation", operation_model.name)
35+
except:
36+
pass
37+
38+
result = await wrapped(*args, **kwargs)
39+
try:
40+
request_id = result[1]["ResponseMetadata"]["RequestId"]
41+
trace._add_agent_attribute("aws.requestId", request_id)
42+
except:
43+
pass
44+
return result
45+
46+
47+
def instrument_aiobotocore_endpoint(module):
48+
wrap_function_wrapper(module, "AioEndpoint.make_request", wrap_endpoint_make_request)

newrelic/hooks/framework_flask.py

+3-6
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,9 @@
2828
from newrelic.api.wsgi_application import wrap_wsgi_application
2929
from newrelic.common.object_names import callable_name
3030
from newrelic.common.object_wrapper import function_wrapper, wrap_function_wrapper
31+
from newrelic.common.package_version_utils import get_package_version
3132

32-
33-
def framework_details():
34-
import flask
35-
36-
return ("Flask", getattr(flask, "__version__", None))
33+
FLASK_VERSION = ("Flask", get_package_version("flask"))
3734

3835

3936
def status_code(exc, value, tb):
@@ -276,7 +273,7 @@ def instrument_flask_views(module):
276273

277274

278275
def instrument_flask_app(module):
279-
wrap_wsgi_application(module, "Flask.wsgi_app", framework=framework_details)
276+
wrap_wsgi_application(module, "Flask.wsgi_app", framework=FLASK_VERSION)
280277

281278
wrap_function_wrapper(module, "Flask.add_url_rule", _nr_wrapper_Flask_add_url_rule_input_)
282279

+151
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
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 logging
17+
import socket
18+
import threading
19+
20+
import moto.server
21+
import werkzeug.serving
22+
from testing_support.fixture.event_loop import ( # noqa: F401, pylint: disable=W0611
23+
event_loop as loop,
24+
)
25+
from testing_support.fixtures import ( # noqa: F401, pylint: disable=W0611
26+
collector_agent_registration_fixture,
27+
collector_available_fixture,
28+
)
29+
30+
PORT = 4443
31+
AWS_ACCESS_KEY_ID = "AAAAAAAAAAAACCESSKEY"
32+
AWS_SECRET_ACCESS_KEY = "AAAAAASECRETKEY" # nosec
33+
HOST = "127.0.0.1"
34+
35+
36+
_default_settings = {
37+
"transaction_tracer.explain_threshold": 0.0,
38+
"transaction_tracer.transaction_threshold": 0.0,
39+
"transaction_tracer.stack_trace_threshold": 0.0,
40+
"debug.log_data_collector_payloads": True,
41+
"debug.record_transaction_failure": True,
42+
}
43+
collector_agent_registration = collector_agent_registration_fixture(
44+
app_name="Python Agent Test (external_aiobotocore)",
45+
default_settings=_default_settings,
46+
linked_applications=["Python Agent Test (external_aiobotocore)"],
47+
)
48+
49+
50+
def get_free_tcp_port(release_socket: bool = False):
51+
sckt = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
52+
sckt.bind((HOST, 0))
53+
_, port = sckt.getsockname() # address, port
54+
if release_socket:
55+
sckt.close()
56+
return port
57+
58+
return sckt, port
59+
60+
61+
class MotoService:
62+
"""Will Create MotoService.
63+
Service is ref-counted so there will only be one per process. Real Service will
64+
be returned by `__aenter__`."""
65+
66+
_services = {} # {name: instance}
67+
68+
def __init__(self, service_name: str, port: int = None, ssl: bool = False):
69+
self._service_name = service_name
70+
71+
if port:
72+
self._socket = None
73+
self._port = port
74+
else:
75+
self._socket, self._port = get_free_tcp_port()
76+
77+
self._thread = None
78+
self._logger = logging.getLogger("MotoService")
79+
self._refcount = None
80+
self._ip_address = HOST
81+
self._server = None
82+
self._ssl_ctx = werkzeug.serving.generate_adhoc_ssl_context() if ssl else None
83+
self._schema = "http" if not self._ssl_ctx else "https"
84+
85+
@property
86+
def endpoint_url(self):
87+
return f"{self._schema}://{self._ip_address}:{self._port}"
88+
89+
def __call__(self, func):
90+
async def wrapper(*args, **kwargs):
91+
await self._start()
92+
try:
93+
result = await func(*args, **kwargs)
94+
finally:
95+
await self._stop()
96+
return result
97+
98+
functools.update_wrapper(wrapper, func)
99+
wrapper.__wrapped__ = func
100+
return wrapper
101+
102+
async def __aenter__(self):
103+
svc = self._services.get(self._service_name)
104+
if svc is None:
105+
self._services[self._service_name] = self
106+
self._refcount = 1
107+
await self._start()
108+
return self
109+
else:
110+
svc._refcount += 1
111+
return svc
112+
113+
async def __aexit__(self, exc_type, exc_val, exc_tb):
114+
self._refcount -= 1
115+
116+
if self._socket:
117+
self._socket.close()
118+
self._socket = None
119+
120+
if self._refcount == 0:
121+
del self._services[self._service_name]
122+
await self._stop()
123+
124+
def _server_entry(self):
125+
self._main_app = moto.server.DomainDispatcherApplication(
126+
moto.server.create_backend_app # , service=self._service_name
127+
)
128+
self._main_app.debug = True
129+
130+
if self._socket:
131+
self._socket.close() # release right before we use it
132+
self._socket = None
133+
134+
self._server = werkzeug.serving.make_server(
135+
self._ip_address,
136+
self._port,
137+
self._main_app,
138+
True,
139+
ssl_context=self._ssl_ctx,
140+
)
141+
self._server.serve_forever()
142+
143+
async def _start(self):
144+
self._thread = threading.Thread(target=self._server_entry, daemon=True)
145+
self._thread.start()
146+
147+
async def _stop(self):
148+
if self._server:
149+
self._server.shutdown()
150+
151+
self._thread.join()

0 commit comments

Comments
 (0)