Skip to content

Commit

Permalink
AIOBotocore instrumentation (#1135)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
lrafeei and mergify[bot] authored May 23, 2024
1 parent 20c5298 commit b75ea43
Show file tree
Hide file tree
Showing 10 changed files with 697 additions and 12 deletions.
11 changes: 5 additions & 6 deletions newrelic/api/web_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@
)
from newrelic.common.object_names import callable_name
from newrelic.common.object_wrapper import FunctionWrapper, wrap_object
from newrelic.core.attribute import create_attributes, process_user_attribute
from newrelic.core.attribute_filter import DST_BROWSER_MONITORING, DST_NONE
from newrelic.core.attribute_filter import DST_BROWSER_MONITORING
from newrelic.packages import six

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

# create the data structure that pull all our data in

broswer_agent_configuration = self.browser_monitoring_intrinsics(obfuscation_key)
browser_agent_configuration = self.browser_monitoring_intrinsics(obfuscation_key)

if attributes:
attributes = obfuscate(json_encode(attributes), obfuscation_key)
broswer_agent_configuration["atts"] = attributes
browser_agent_configuration["atts"] = attributes

header = _js_agent_header_fragment % (
_encode_nonce(nonce),
json_encode(broswer_agent_configuration),
json_encode(browser_agent_configuration),
self._settings.js_agent_loader,
)

Expand Down Expand Up @@ -568,7 +567,7 @@ def __iter__(self):
yield "content-length", self.environ["CONTENT_LENGTH"]
elif key == "CONTENT_TYPE":
yield "content-type", self.environ["CONTENT_TYPE"]
elif key == "HTTP_CONTENT_LENGTH" or key == "HTTP_CONTENT_TYPE":
elif key in ("HTTP_CONTENT_LENGTH", "HTTP_CONTENT_TYPE"):
# These keys are illegal and should be ignored
continue
elif key.startswith("HTTP_"):
Expand Down
6 changes: 6 additions & 0 deletions newrelic/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4508,6 +4508,12 @@ def _process_module_builtin_defaults():
"instrument_gearman_worker",
)

_process_module_definition(
"aiobotocore.endpoint",
"newrelic.hooks.external_aiobotocore",
"instrument_aiobotocore_endpoint",
)

_process_module_definition(
"botocore.endpoint",
"newrelic.hooks.external_botocore",
Expand Down
48 changes: 48 additions & 0 deletions newrelic/hooks/external_aiobotocore.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Copyright 2010 New Relic, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from newrelic.api.external_trace import ExternalTrace
from newrelic.common.object_wrapper import wrap_function_wrapper


def _bind_make_request_params(operation_model, request_dict, *args, **kwargs):
return operation_model, request_dict


def bind__send_request(request_dict, operation_model, *args, **kwargs):
return operation_model, request_dict


async def wrap_endpoint_make_request(wrapped, instance, args, kwargs):
operation_model, request_dict = _bind_make_request_params(*args, **kwargs)
url = request_dict.get("url")
method = request_dict.get("method")

with ExternalTrace(library="aiobotocore", url=url, method=method, source=wrapped) as trace:
try:
trace._add_agent_attribute("aws.operation", operation_model.name)
except:
pass

result = await wrapped(*args, **kwargs)
try:
request_id = result[1]["ResponseMetadata"]["RequestId"]
trace._add_agent_attribute("aws.requestId", request_id)
except:
pass
return result


def instrument_aiobotocore_endpoint(module):
wrap_function_wrapper(module, "AioEndpoint.make_request", wrap_endpoint_make_request)
9 changes: 3 additions & 6 deletions newrelic/hooks/framework_flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,9 @@
from newrelic.api.wsgi_application import wrap_wsgi_application
from newrelic.common.object_names import callable_name
from newrelic.common.object_wrapper import function_wrapper, wrap_function_wrapper
from newrelic.common.package_version_utils import get_package_version


def framework_details():
import flask

return ("Flask", getattr(flask, "__version__", None))
FLASK_VERSION = ("Flask", get_package_version("flask"))


def status_code(exc, value, tb):
Expand Down Expand Up @@ -276,7 +273,7 @@ def instrument_flask_views(module):


def instrument_flask_app(module):
wrap_wsgi_application(module, "Flask.wsgi_app", framework=framework_details)
wrap_wsgi_application(module, "Flask.wsgi_app", framework=FLASK_VERSION)

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

Expand Down
151 changes: 151 additions & 0 deletions tests/external_aiobotocore/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Copyright 2010 New Relic, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import functools
import logging
import socket
import threading

import moto.server
import werkzeug.serving
from testing_support.fixture.event_loop import ( # noqa: F401, pylint: disable=W0611
event_loop as loop,
)
from testing_support.fixtures import ( # noqa: F401, pylint: disable=W0611
collector_agent_registration_fixture,
collector_available_fixture,
)

PORT = 4443
AWS_ACCESS_KEY_ID = "AAAAAAAAAAAACCESSKEY"
AWS_SECRET_ACCESS_KEY = "AAAAAASECRETKEY" # nosec
HOST = "127.0.0.1"


_default_settings = {
"transaction_tracer.explain_threshold": 0.0,
"transaction_tracer.transaction_threshold": 0.0,
"transaction_tracer.stack_trace_threshold": 0.0,
"debug.log_data_collector_payloads": True,
"debug.record_transaction_failure": True,
}
collector_agent_registration = collector_agent_registration_fixture(
app_name="Python Agent Test (external_aiobotocore)",
default_settings=_default_settings,
linked_applications=["Python Agent Test (external_aiobotocore)"],
)


def get_free_tcp_port(release_socket: bool = False):
sckt = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sckt.bind((HOST, 0))
_, port = sckt.getsockname() # address, port
if release_socket:
sckt.close()
return port

return sckt, port


class MotoService:
"""Will Create MotoService.
Service is ref-counted so there will only be one per process. Real Service will
be returned by `__aenter__`."""

_services = {} # {name: instance}

def __init__(self, service_name: str, port: int = None, ssl: bool = False):
self._service_name = service_name

if port:
self._socket = None
self._port = port
else:
self._socket, self._port = get_free_tcp_port()

self._thread = None
self._logger = logging.getLogger("MotoService")
self._refcount = None
self._ip_address = HOST
self._server = None
self._ssl_ctx = werkzeug.serving.generate_adhoc_ssl_context() if ssl else None
self._schema = "http" if not self._ssl_ctx else "https"

@property
def endpoint_url(self):
return f"{self._schema}://{self._ip_address}:{self._port}"

def __call__(self, func):
async def wrapper(*args, **kwargs):
await self._start()
try:
result = await func(*args, **kwargs)
finally:
await self._stop()
return result

functools.update_wrapper(wrapper, func)
wrapper.__wrapped__ = func
return wrapper

async def __aenter__(self):
svc = self._services.get(self._service_name)
if svc is None:
self._services[self._service_name] = self
self._refcount = 1
await self._start()
return self
else:
svc._refcount += 1
return svc

async def __aexit__(self, exc_type, exc_val, exc_tb):
self._refcount -= 1

if self._socket:
self._socket.close()
self._socket = None

if self._refcount == 0:
del self._services[self._service_name]
await self._stop()

def _server_entry(self):
self._main_app = moto.server.DomainDispatcherApplication(
moto.server.create_backend_app # , service=self._service_name
)
self._main_app.debug = True

if self._socket:
self._socket.close() # release right before we use it
self._socket = None

self._server = werkzeug.serving.make_server(
self._ip_address,
self._port,
self._main_app,
True,
ssl_context=self._ssl_ctx,
)
self._server.serve_forever()

async def _start(self):
self._thread = threading.Thread(target=self._server_entry, daemon=True)
self._thread.start()

async def _stop(self):
if self._server:
self._server.shutdown()

self._thread.join()
Loading

0 comments on commit b75ea43

Please sign in to comment.