From 1db5c4d50b10940f27b8d5682facddf03e66455b Mon Sep 17 00:00:00 2001 From: Michael Mantos Date: Tue, 27 Jul 2021 04:27:30 +0800 Subject: [PATCH 1/4] LNX-329 - Initial commit for RUMv2 support --- docker/stackify-python-api-test | 3 + stackify/compat.py | 74 ++++++++ stackify/config.py | 6 + stackify/constants.py | 3 + stackify/rum.py | 74 ++++++++ stackify/transport/application.py | 44 +++++ stackify/utils.py | 21 +++ test-docker.sh | 2 +- tests/bases.py | 2 + tests/test_compat.py | 38 ++++ tests/test_rum.py | 287 ++++++++++++++++++++++++++++++ tests/test_utils.py | 25 +++ 12 files changed, 578 insertions(+), 1 deletion(-) create mode 100644 stackify/compat.py create mode 100644 stackify/config.py create mode 100644 stackify/rum.py create mode 100644 tests/test_compat.py create mode 100644 tests/test_rum.py diff --git a/docker/stackify-python-api-test b/docker/stackify-python-api-test index d7d7957..5b8f88f 100644 --- a/docker/stackify-python-api-test +++ b/docker/stackify-python-api-test @@ -3,6 +3,8 @@ ARG from_version FROM python:${from_version} ARG version +ARG test +ARG test_repo RUN \ apt-get update && \ @@ -13,5 +15,6 @@ RUN mkdir /build COPY . /build/ RUN cat /build/requirements.txt | xargs -n 1 pip install; exit 0 +RUN if [ "${test}" = 1 ]; then pip install -i "${test_repo}" stackify-python-apm; fi; exit 0 CMD /bin/bash -c "cd /build && source test-docker-execute.sh" diff --git a/stackify/compat.py b/stackify/compat.py new file mode 100644 index 0000000..6aa68f7 --- /dev/null +++ b/stackify/compat.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +import sys +import types + +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 + + +if PY2: + import StringIO + import Queue as queue # noqa F401 + import urlparse # noqa F401 + from urllib2 import HTTPError # noqa F401 + from urllib import unquote as unquote_core # noqa F401 + + StringIO = BytesIO = StringIO.StringIO + + string_types = (basestring,) # noqa F821 + integer_types = (int, long) # noqa F821 + class_types = (type, types.ClassType) + text_type = unicode # noqa F821 + binary_type = str + list_type = list + dict_type = dict + + def b(s): + return s + + def iterkeys(d, **kwargs): + return d.iterkeys(**kwargs) + + def iteritems(d, **kwargs): + return d.iteritems(**kwargs) + + def iterlists(d, **kwargs): + return d.iterlists(**kwargs) + + def unquote(*args, **kwargs): # noqa F811 + return unquote_core(*args, **kwargs) +else: + import io + import queue # noqa F401 + from urllib import parse as urlparse # noqa F401 + from urllib.error import HTTPError # noqa F401 + + StringIO = io.StringIO + BytesIO = io.BytesIO + + string_types = (str,) + integer_types = (int,) + class_types = (type,) + text_type = str + binary_type = bytes + list_type = list + dict_type = dict + + def b(s): + return s.encode("latin-1") + + def iterkeys(d, **kwargs): + return iter(d.keys(**kwargs)) + + def iteritems(d, **kwargs): + return iter(d.items(**kwargs)) + + def iterlists(d, **kwargs): + return iter(d.lists(**kwargs)) + + def unquote(*args, **kwargs): + return urlparse.unquote(*args, **kwargs) + + +def multidict_to_dict(d): + return dict((k, v[0] if len(v) == 1 else v) for k, v in iterlists(d)) diff --git a/stackify/config.py b/stackify/config.py new file mode 100644 index 0000000..58d5fa3 --- /dev/null +++ b/stackify/config.py @@ -0,0 +1,6 @@ +from stackify.constants import DEFAULT_RUM_KEY, DEFAULT_RUM_SCRIPT_URL + +rum_key = DEFAULT_RUM_KEY +rum_script_url = DEFAULT_RUM_SCRIPT_URL +application = None +environment = None diff --git a/stackify/constants.py b/stackify/constants.py index 3dce49e..c560063 100644 --- a/stackify/constants.py +++ b/stackify/constants.py @@ -38,3 +38,6 @@ TRANSPORT_TYPE_DEFAULT = 'default' TRANSPORT_TYPE_AGENT_SOCKET = 'agent_socket' TRANSPORT_TYPE_AGENT_HTTP = 'agent_http' + +DEFAULT_RUM_SCRIPT_URL = "https://stckjs.stackify.com/stckjs.js" +DEFAULT_RUM_KEY = "" diff --git a/stackify/rum.py b/stackify/rum.py new file mode 100644 index 0000000..6122bf4 --- /dev/null +++ b/stackify/rum.py @@ -0,0 +1,74 @@ +import json +import base64 +from stackify import config + +apm_installed = False + +try: + apm_installed = True + from stackifyapm import insert_rum_script as insert_rum_script_apm +except ImportError: + pass + + +def insert_rum_script(): + if apm_installed is True: + return insert_rum_script_apm() + + rum_key = config.rum_key + rum_script_url = config.rum_script_url + + if not rum_script_url or not rum_key: + return None + + transaction_id = get_transaction_id() + if not transaction_id: + return None + + reporting_url = get_reporting_url() + if not reporting_url: + return None + + application_name = config.application + if not application_name: + return None + + environment = config.environment + if not environment: + return None + + settings = { + "ID": transaction_id + } + + if application_name: + application_name_b64 = base64.b64encode(application_name.encode("utf-8")).decode("utf-8") + if (application_name_b64): + settings["Name"] = application_name_b64 + + if environment: + environment_b64 = base64.b64encode(environment.encode("utf-8")).decode("utf-8") + if (environment_b64): + settings["Env"] = environment_b64 + + if reporting_url: + reporting_url_b64 = base64.b64encode(reporting_url.encode("utf-8")).decode("utf-8") + if (reporting_url_b64): + settings["Trans"] = reporting_url_b64 + + if not settings: + return None + + return ''.format( + json.dumps(settings), + rum_script_url, + rum_key + ) + + +def get_transaction_id(): + return '' + + +def get_reporting_url(): + return '' diff --git a/stackify/transport/application.py b/stackify/transport/application.py index ac11947..bcbdd66 100644 --- a/stackify/transport/application.py +++ b/stackify/transport/application.py @@ -1,5 +1,6 @@ import socket import os +import logging from stackify.utils import arg_or_env from stackify.constants import API_URL @@ -9,6 +10,12 @@ from stackify.constants import TRANSPORT_TYPE_AGENT_SOCKET from stackify.constants import TRANSPORT_TYPE_DEFAULT from stackify.transport.default.formats import JSONObject +from stackify.constants import DEFAULT_RUM_SCRIPT_URL +from stackify.constants import DEFAULT_RUM_KEY +from stackify.utils import RegexValidator, ConfigError +from stackify import config + +internal_logger = logging.getLogger(__name__) class EnvironmentDetail(JSONObject): @@ -38,6 +45,8 @@ def __init__( socket_url=SOCKET_URL, transport=None, http_endpoint=DEFAULT_HTTP_ENDPOINT, + rum_script_url=DEFAULT_RUM_SCRIPT_URL, + rum_key=DEFAULT_RUM_KEY ): self.api_key = api_key self.api_url = api_url @@ -47,6 +56,39 @@ def __init__( self.http_endpoint = http_endpoint self.transport = transport + self.rum_script_url = DEFAULT_RUM_SCRIPT_URL + self.rum_key = DEFAULT_RUM_KEY + + # Rum config validation + if rum_script_url != DEFAULT_RUM_SCRIPT_URL: + self.validate( + RegexValidator("^((((https?|ftps?|gopher|telnet|nntp)://)|(mailto:|news:))(%[0-9A-Fa-f]{2}|[-\(\)_.!~*';/?:@&=+$,A-Za-z0-9])+)([).!';/?:,][\[:blank:|:blank:\]])?$"), + rum_script_url, + 'rum_script_url' + ) + config.rum_script_url = self.rum_script_url + + if rum_key != DEFAULT_RUM_KEY: + self.validate( + RegexValidator("^[A-Za-z0-9_-]+$"), + rum_key, + 'rum_key' + ) + config.rum_key = self.rum_key + + config.environment = self.environment + config.application = self.application + + def validate(self, validator, value, key): + if not validator: + return + + try: + value = validator(value, key) + setattr(self, key, str(value)) + except ConfigError as e: + internal_logger.exception(str(e)) + def get_configuration(**kwargs): """ @@ -69,4 +111,6 @@ def get_configuration(**kwargs): socket_url=arg_or_env('socket_url', kwargs, SOCKET_URL), http_endpoint=arg_or_env('http_endpoint', kwargs, DEFAULT_HTTP_ENDPOINT, env_key='STACKIFY_TRANSPORT_HTTP_ENDPOINT'), transport=transport, + rum_script_url=arg_or_env('rum_script_url', kwargs, DEFAULT_RUM_SCRIPT_URL, env_key='RETRACE_RUM_SCRIPT_URL'), + rum_key=arg_or_env('rum_key', kwargs, DEFAULT_RUM_KEY, env_key='RETRACE_RUM_KEY') ) diff --git a/stackify/utils.py b/stackify/utils.py index 2da8944..0a092a3 100644 --- a/stackify/utils.py +++ b/stackify/utils.py @@ -1,6 +1,8 @@ import os import json import logging +import re +from stackify import compat internal_logger = logging.getLogger(__name__) @@ -36,3 +38,22 @@ def get_default_object(obj): def object_is_iterable(obj): return hasattr(obj, '__iter__') or isinstance(obj, str) + + +class RegexValidator(object): + def __init__(self, regex, verbose_pattern=None): + self.regex = regex + self.verbose_pattern = verbose_pattern or regex + + def __call__(self, value, field_name): + value = compat.text_type(value) + match = re.match(self.regex, value) + if match: + return value + raise ConfigError("{} does not match pattern {}".format(value, self.verbose_pattern), field_name) + + +class ConfigError(ValueError): + def __init__(self, msg, field_name): + self.field_name = field_name + super(ValueError, self).__init__(msg) diff --git a/test-docker.sh b/test-docker.sh index 5e0bbce..e4f4dea 100755 --- a/test-docker.sh +++ b/test-docker.sh @@ -18,7 +18,7 @@ do fi echo "Building stackify-python-api-test-${i}..." - docker build --no-cache --build-arg from_version=${i} --build-arg version=${i} --file docker/stackify-python-api-test . -t stackify-python-api-test-${i}:latest + docker build --no-cache --build-arg from_version=${i} --build-arg version=${i} --build-arg test=${TEST} --build-arg test_repo=${TEST_REPO} --file docker/stackify-python-api-test . -t stackify-python-api-test-${i}:latest echo "Running stackify-python-api-test-${i}..." docker run --network="host" --name "stackify-python-api-test-${i}" stackify-python-api-test-${i}:latest diff --git a/tests/bases.py b/tests/bases.py index 0b11585..d7298bd 100644 --- a/tests/bases.py +++ b/tests/bases.py @@ -23,6 +23,8 @@ def setUp(self): 'STACKIFY_API_URL', 'STACKIFY_TRANSPORT', 'STACKIFY_TRANSPORT_HTTP_ENDPOINT', + 'RETRACE_RUM_SCRIPT_URL', + 'RETRACE_RUM_KEY' ] self.saved = {} for key in to_save: diff --git a/tests/test_compat.py b/tests/test_compat.py new file mode 100644 index 0000000..9f97bbb --- /dev/null +++ b/tests/test_compat.py @@ -0,0 +1,38 @@ +from unittest import TestCase + +from stackifyapm.utils.compat import b +from stackifyapm.utils.compat import iterkeys +from stackifyapm.utils.compat import iteritems + + +class CompatTest(TestCase): + def setUp(self): + self.dict_data = { + "key1": "value1", + "key2": "value2" + } + self.list_data = ["foo", "bar"] + + def test_convert_string_to_byte(self): + byte = '1' + + value = b(byte) + + assert isinstance(value, bytes) + + def test_iterkeys_should_return_iterator(self): + iter_keys = iterkeys(self.dict_data) + + self.assert_instance_is_an_iterator(iter_keys) + + def test_iteritems_should_return_iterator(self): + iter_items = iteritems(self.dict_data) + + self.assert_instance_is_an_iterator(iter_items) + + def assert_instance_is_an_iterator(self, item): + try: + iter(item) + assert True + except Exception: + assert False diff --git a/tests/test_rum.py b/tests/test_rum.py new file mode 100644 index 0000000..bb9cc82 --- /dev/null +++ b/tests/test_rum.py @@ -0,0 +1,287 @@ +try: + from unittest import mock +except Exception: + import mock # noqa F401 + +import stackify +import os +import stackify.rum +import base64 +import json + +from .bases import ClearEnvTest +from stackify.transport.application import ApiConfiguration +from stackify.utils import arg_or_env +from stackify.constants import DEFAULT_RUM_SCRIPT_URL +from stackifyapm.base import Client +from unittest import TestCase + +APM_CONFIG = { + "SERVICE_NAME": "service_name", + "ENVIRONMENT": "production", + "HOSTNAME": "sample_host", + "FRAMEWORK_NAME": "framework", + "FRAMEWORK_VERSION": "1.0", + "APPLICATION_NAME": "sample_application", + "BASE_DIR": "path/to/application/", + "RUM_SCRIPT_URL": "https://test.com/test.js", + "RUM_KEY": "LOREM123" +} + + +class RumTest(TestCase): + def setUp(self): + self.config_rum_key = stackify.config.rum_key + self.config_rum_script_url = stackify.config.rum_script_url + self.config_application = stackify.config.application + self.config_environment = stackify.config.environment + self.maxDiff = None + + def shutDown(self): + pass + + def test_default_insert_rum_script_from_apm_with_transaction(self): + self.update_apm_installed(True) + client = Client(APM_CONFIG) + + transaction = client.begin_transaction("transaction_test", client=client) + rum_data = stackify.rum.insert_rum_script() + assert rum_data + + rum_settings = { + "ID": transaction.get_trace_parent().trace_id, + "Name": base64.b64encode(APM_CONFIG["APPLICATION_NAME"].encode("utf-8")).decode("utf-8"), + "Env": base64.b64encode(APM_CONFIG["ENVIRONMENT"].encode("utf-8")).decode("utf-8"), + "Trans": base64.b64encode('/'.encode("utf-8")).decode("utf-8") + } + + result_string = ''.format( + json.dumps(rum_settings), + APM_CONFIG["RUM_SCRIPT_URL"], + APM_CONFIG["RUM_KEY"] + ) + + assert rum_data == result_string + client.end_transaction("transaction_test") + self.restore_apm_installed() + + def test_default_insert_rum_script_from_apm_without_transaction(self): + self.update_apm_installed(True) + rum_data = stackify.rum.insert_rum_script() + assert not rum_data + self.restore_apm_installed() + + @mock.patch('stackify.rum.get_reporting_url') + @mock.patch('stackify.rum.get_transaction_id') + def test_default_insert_rum_script(self, func, func_reporting_url): + func.return_value = '123' + func_reporting_url.return_value = 'test reporting url' + self.update_apm_installed(False) + self.update_common_config( + rum_key='asd', + application='app', + environment='env' + ) + + rum_settings = { + "ID": '123', + "Name": 'YXBw', + "Env": 'ZW52', + "Trans": 'dGVzdCByZXBvcnRpbmcgdXJs' + } + + result = stackify.rum.insert_rum_script() + self.reset_common_config() + self.restore_apm_installed() + + assert result == ''.format(json.dumps(rum_settings)) + + def test_default_insert_rum_script_no_transaction_id(self): + self.update_apm_installed(False) + self.update_common_config( + rum_key='asd', + application='app', + environment='env' + ) + + result = stackify.rum.insert_rum_script() + self.reset_common_config() + self.restore_apm_installed() + + assert result is None + + def test_default_insert_rum_script_no_key(self): + self.update_apm_installed(False) + self.update_common_config( + rum_key='', + application='app', + environment='env' + ) + + result = stackify.rum.insert_rum_script() + assert not result + + self.reset_common_config() + self.restore_apm_installed() + + def test_default_insert_rum_script_no_details(self): + self.update_apm_installed(False) + self.update_common_config() + + result = stackify.rum.insert_rum_script() + assert not result + + self.reset_common_config() + self.restore_apm_installed() + + @mock.patch('stackify.rum.get_reporting_url') + @mock.patch('stackify.rum.get_transaction_id') + def test_default_insert_rum_script_from_api(self, func, func_reporting_url): + func.return_value = '123' + func_reporting_url.return_value = 'test reporting url' + self.update_apm_installed(False) + self.create_config( + rum_key='asd1', + application='app1', + environment='env1' + ) + rum_settings = { + "ID": '123', + "Name": 'YXBwMQ==', + "Env": 'ZW52MQ==', + "Trans": 'dGVzdCByZXBvcnRpbmcgdXJs' + } + result = stackify.rum.insert_rum_script() + self.reset_common_config() + self.restore_apm_installed() + assert result == ''.format(json.dumps(rum_settings)) + + def test_default_insert_rum_script_no_key_from_api(self): + self.update_apm_installed(False) + self.create_config( + rum_key=None, + application='app2', + environment='env2' + ) + + result = stackify.rum.insert_rum_script() + self.reset_common_config() + self.restore_apm_installed() + + assert not result + + def test_default_insert_rum_script_no_details_from_api(self): + self.update_apm_installed(False) + self.create_config( + application=None, + environment=None, + rum_key=None + ) + + result = stackify.rum.insert_rum_script() + self.reset_common_config() + self.restore_apm_installed() + + assert not result + + def update_apm_installed(self, installed): + self.apm_installed = stackify.rum.apm_installed + stackify.rum.apm_installed = installed + + def restore_apm_installed(self): + stackify.rum.apm_installed = self.apm_installed + + def update_common_config(self, rum_key=None, rum_script_url=None, application=None, environment=None): + self.config_rum_key = stackify.config.rum_key + self.config_rum_script_url = stackify.config.rum_script_url + self.config_application = stackify.config.application + self.config_environment = stackify.config.environment + + if rum_key is not None: + stackify.config.rum_key = rum_key + if rum_script_url is not None: + stackify.config.rum_script_url = rum_script_url + if application is not None: + stackify.config.application = application + if environment is not None: + stackify.config.environment = environment + + def reset_common_config(self): + stackify.config.rum_key = self.config_rum_key + stackify.config.rum_script_url = self.config_rum_script_url + stackify.config.application = self.config_application + stackify.config.environment = self.config_environment + + def create_config(self, **kwargs): + return ApiConfiguration( + application=kwargs['application'], + environment=kwargs['environment'], + api_key='test_apikey', + api_url='test_apiurl', + rum_script_url=arg_or_env('rum_script_url', kwargs, DEFAULT_RUM_SCRIPT_URL, env_key='RETRACE_RUM_SCRIPT_URL'), + rum_key=arg_or_env('rum_key', kwargs, DEFAULT_RUM_SCRIPT_URL, env_key='RETRACE_RUM_KEY') + ) + + +class RumConfigurationTest(ClearEnvTest): + ''' + Test the logger init functionality + ''' + + def setUp(self): + super(RumConfigurationTest, self).setUp() + self.config_rum_key = stackify.config.rum_key + self.config_rum_script_url = stackify.config.rum_script_url + self.config_application = stackify.config.application + self.config_environment = stackify.config.environment + + def tearDown(self): + super(RumConfigurationTest, self).tearDown() + + def test_rum_script_url_valid(self): + os.environ["RETRACE_RUM_SCRIPT_URL"] = 'https://test.com/test.js' + config = self.create_config() + assert config.rum_script_url == 'https://test.com/test.js' + del os.environ["RETRACE_RUM_SCRIPT_URL"] + self.reset_common_config() + + @mock.patch('logging.Logger.exception') + def test_rum_script_url_invalid(self, func=None): + os.environ["RETRACE_RUM_SCRIPT_URL"] = 'asd' + config = self.create_config() + assert config.rum_script_url == 'https://stckjs.stackify.com/stckjs.js' # Default + del os.environ["RETRACE_RUM_SCRIPT_URL"] + self.reset_common_config() + func.assert_called_with('https://stckjs.stackify.com/stckjs.js does not match pattern ^[A-Za-z0-9_-]+$') + + def test_rum_key_valid(self): + os.environ["RETRACE_RUM_KEY"] = 'TEST123-_' + config = self.create_config() + assert config.rum_key == 'TEST123-_' + del os.environ["RETRACE_RUM_KEY"] + self.reset_common_config() + + @mock.patch('logging.Logger.exception') + def test_rum_key_invalid(self, func=None): + os.environ["RETRACE_RUM_KEY"] = 'asd`1!' + config = self.create_config() + assert config.rum_key == '' # Default + del os.environ["RETRACE_RUM_KEY"] + self.reset_common_config() + func.assert_called_with('asd`1! does not match pattern ^[A-Za-z0-9_-]+$') + + def create_config(self, **kwargs): + return ApiConfiguration( + application='test_appname', + environment='test_environment', + api_key='test_apikey', + api_url='test_apiurl', + rum_script_url=arg_or_env('rum_script_url', kwargs, DEFAULT_RUM_SCRIPT_URL, env_key='RETRACE_RUM_SCRIPT_URL'), + rum_key=arg_or_env('rum_key', kwargs, DEFAULT_RUM_SCRIPT_URL, env_key='RETRACE_RUM_KEY') + ) + + def reset_common_config(self): + stackify.config.rum_key = self.config_rum_key + stackify.config.rum_script_url = self.config_rum_script_url + stackify.config.application = self.config_application + stackify.config.environment = self.config_environment diff --git a/tests/test_utils.py b/tests/test_utils.py index 70e4b78..2ec637c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -9,6 +9,9 @@ import stackify import logging +from stackify.utils import ConfigError +from stackify.utils import RegexValidator + class TestInit(ClearEnvTest): ''' @@ -91,6 +94,28 @@ def test_utils_data_to_json_dummy_request(self): self.assertTrue(substring in result) +class RegexValidatorTest(ClearEnvTest): + + def test_should_return_correct_value(self): + regex = "^[a-zA-Z0-9 _-]+$" + value = 'some_value' + _validate = RegexValidator(regex) + + validated_value = _validate(value, 'SOME_KEY') + + assert validated_value == value + + def test_should_raise_exception(self): + regex = "^[a-zA-Z0-9 _-]+$" + value = '#$%^' + _validate = RegexValidator(regex) + + with self.assertRaises(ConfigError) as context: + _validate(value, 'SOME_KEY') + + assert 'does not match pattern' in context.exception.args[0] + + class Dummy(object): pass From 9a0b65baf7e6aa7e50b89c3c16b2c1985e9e0055 Mon Sep 17 00:00:00 2001 From: Michael Mantos Date: Fri, 30 Jul 2021 22:27:44 +0800 Subject: [PATCH 2/4] LNX-329 - Fix MethodNotFound error for profiler rum checking --- docker/stackify-python-api-test | 4 +- stackify/rum.py | 20 ++++++-- test-docker-execute.sh | 11 ++++- tests/rum/__init__.py | 0 tests/rum/test_rum_apm.py | 76 ++++++++++++++++++++++++++++++ tests/test_compat.py | 6 +-- tests/test_rum.py | 82 +++++++++------------------------ 7 files changed, 129 insertions(+), 70 deletions(-) create mode 100644 tests/rum/__init__.py create mode 100644 tests/rum/test_rum_apm.py diff --git a/docker/stackify-python-api-test b/docker/stackify-python-api-test index 5b8f88f..7383754 100644 --- a/docker/stackify-python-api-test +++ b/docker/stackify-python-api-test @@ -15,6 +15,8 @@ RUN mkdir /build COPY . /build/ RUN cat /build/requirements.txt | xargs -n 1 pip install; exit 0 -RUN if [ "${test}" = 1 ]; then pip install -i "${test_repo}" stackify-python-apm; fi; exit 0 + +ENV TEST="${test}" +ENV TEST_REPO="${test_repo}" CMD /bin/bash -c "cd /build && source test-docker-execute.sh" diff --git a/stackify/rum.py b/stackify/rum.py index 6122bf4..50350df 100644 --- a/stackify/rum.py +++ b/stackify/rum.py @@ -5,15 +5,16 @@ apm_installed = False try: + from stackifyapm import insert_rum_script as insert_rum_script_from_apm apm_installed = True - from stackifyapm import insert_rum_script as insert_rum_script_apm -except ImportError: +except (ImportError): pass def insert_rum_script(): - if apm_installed is True: - return insert_rum_script_apm() + apm_rum_script = insert_rum_script_apm() + if apm_rum_script: + return apm_rum_script rum_key = config.rum_key rum_script_url = config.rum_script_url @@ -72,3 +73,14 @@ def get_transaction_id(): def get_reporting_url(): return '' + + +def insert_rum_script_apm(): + if not is_apm_installed(): + return + + return insert_rum_script_from_apm() + + +def is_apm_installed(): + return apm_installed diff --git a/test-docker-execute.sh b/test-docker-execute.sh index fc76dce..f87161f 100755 --- a/test-docker-execute.sh +++ b/test-docker-execute.sh @@ -14,7 +14,16 @@ function runPyTest() { echo '<--------------------------------------------->' echo "Python Version $(python --version)" echo 'Running pytest...' - py.test + py.test --ignore=tests/rum + + if [ "${TEST}" = 1 ]; then + pip install -i "${TEST_REPO}" stackify-python-apm; + else + pip install stackify-python-apm; + py.test tests/rum + fi + + pip uninstall -y stackify-python-apm } runFlake8 diff --git a/tests/rum/__init__.py b/tests/rum/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/rum/test_rum_apm.py b/tests/rum/test_rum_apm.py new file mode 100644 index 0000000..fc8a28d --- /dev/null +++ b/tests/rum/test_rum_apm.py @@ -0,0 +1,76 @@ +try: + from unittest import mock +except Exception: + import mock # noqa F401 + +import stackify +import stackify.rum +import base64 +import json +from unittest import TestCase + +apmExist = False +try: + from stackifyapm.base import Client + apmExist = True +except (ImportError): + pass + +APM_CONFIG = { + "SERVICE_NAME": "service_name", + "ENVIRONMENT": "production", + "HOSTNAME": "sample_host", + "FRAMEWORK_NAME": "framework", + "FRAMEWORK_VERSION": "1.0", + "APPLICATION_NAME": "sample_application", + "BASE_DIR": "path/to/application/", + "RUM_SCRIPT_URL": "https://test.com/test.js", + "RUM_KEY": "LOREM123" +} + + +class RumTestApm(TestCase): + def setUp(self): + self.config_rum_key = stackify.config.rum_key + self.config_rum_script_url = stackify.config.rum_script_url + self.config_application = stackify.config.application + self.config_environment = stackify.config.environment + self.maxDiff = None + + def shutDown(self): + pass + + def test_default_insert_rum_script_from_apm_with_transaction(self): + if not apmExist: + return + + client = Client(APM_CONFIG) + print('config') + print(client.config.rum_key) + + transaction = client.begin_transaction("transaction_test", client=client) + rum_data = stackify.rum.insert_rum_script() + + rum_settings = { + "ID": transaction.get_trace_parent().trace_id, + "Name": base64.b64encode(APM_CONFIG["APPLICATION_NAME"].encode("utf-8")).decode("utf-8"), + "Env": base64.b64encode(APM_CONFIG["ENVIRONMENT"].encode("utf-8")).decode("utf-8"), + "Trans": base64.b64encode('/'.encode("utf-8")).decode("utf-8") + } + + result_string = ''.format( + json.dumps(rum_settings), + APM_CONFIG["RUM_SCRIPT_URL"], + APM_CONFIG["RUM_KEY"] + ) + + assert rum_data + assert rum_data == result_string + client.end_transaction("transaction_test") + + def test_default_insert_rum_script_from_apm_without_transaction(self): + if not apmExist: + return + + rum_data = stackify.rum.insert_rum_script() + assert not rum_data diff --git a/tests/test_compat.py b/tests/test_compat.py index 9f97bbb..d2dcfbd 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -1,8 +1,8 @@ from unittest import TestCase -from stackifyapm.utils.compat import b -from stackifyapm.utils.compat import iterkeys -from stackifyapm.utils.compat import iteritems +from stackify.compat import b +from stackify.compat import iterkeys +from stackify.compat import iteritems class CompatTest(TestCase): diff --git a/tests/test_rum.py b/tests/test_rum.py index bb9cc82..17fc683 100644 --- a/tests/test_rum.py +++ b/tests/test_rum.py @@ -6,14 +6,12 @@ import stackify import os import stackify.rum -import base64 import json from .bases import ClearEnvTest from stackify.transport.application import ApiConfiguration from stackify.utils import arg_or_env from stackify.constants import DEFAULT_RUM_SCRIPT_URL -from stackifyapm.base import Client from unittest import TestCase APM_CONFIG = { @@ -40,43 +38,13 @@ def setUp(self): def shutDown(self): pass - def test_default_insert_rum_script_from_apm_with_transaction(self): - self.update_apm_installed(True) - client = Client(APM_CONFIG) - - transaction = client.begin_transaction("transaction_test", client=client) - rum_data = stackify.rum.insert_rum_script() - assert rum_data - - rum_settings = { - "ID": transaction.get_trace_parent().trace_id, - "Name": base64.b64encode(APM_CONFIG["APPLICATION_NAME"].encode("utf-8")).decode("utf-8"), - "Env": base64.b64encode(APM_CONFIG["ENVIRONMENT"].encode("utf-8")).decode("utf-8"), - "Trans": base64.b64encode('/'.encode("utf-8")).decode("utf-8") - } - - result_string = ''.format( - json.dumps(rum_settings), - APM_CONFIG["RUM_SCRIPT_URL"], - APM_CONFIG["RUM_KEY"] - ) - - assert rum_data == result_string - client.end_transaction("transaction_test") - self.restore_apm_installed() - - def test_default_insert_rum_script_from_apm_without_transaction(self): - self.update_apm_installed(True) - rum_data = stackify.rum.insert_rum_script() - assert not rum_data - self.restore_apm_installed() - + @mock.patch('stackify.rum.is_apm_installed') @mock.patch('stackify.rum.get_reporting_url') @mock.patch('stackify.rum.get_transaction_id') - def test_default_insert_rum_script(self, func, func_reporting_url): + def test_default_insert_rum_script(self, func, func_reporting_url, func_apm): func.return_value = '123' func_reporting_url.return_value = 'test reporting url' - self.update_apm_installed(False) + func_apm.return_value = False self.update_common_config( rum_key='asd', application='app', @@ -92,12 +60,12 @@ def test_default_insert_rum_script(self, func, func_reporting_url): result = stackify.rum.insert_rum_script() self.reset_common_config() - self.restore_apm_installed() assert result == ''.format(json.dumps(rum_settings)) - def test_default_insert_rum_script_no_transaction_id(self): - self.update_apm_installed(False) + @mock.patch('stackify.rum.is_apm_installed') + def test_default_insert_rum_script_no_transaction_id(self, func_apm): + func_apm.return_value = False self.update_common_config( rum_key='asd', application='app', @@ -106,12 +74,12 @@ def test_default_insert_rum_script_no_transaction_id(self): result = stackify.rum.insert_rum_script() self.reset_common_config() - self.restore_apm_installed() assert result is None - def test_default_insert_rum_script_no_key(self): - self.update_apm_installed(False) + @mock.patch('stackify.rum.is_apm_installed') + def test_default_insert_rum_script_no_key(self, func_apm): + func_apm.return_value = False self.update_common_config( rum_key='', application='app', @@ -122,24 +90,24 @@ def test_default_insert_rum_script_no_key(self): assert not result self.reset_common_config() - self.restore_apm_installed() - def test_default_insert_rum_script_no_details(self): - self.update_apm_installed(False) + @mock.patch('stackify.rum.is_apm_installed') + def test_default_insert_rum_script_no_details(self, func_apm): + func_apm.return_value = False self.update_common_config() result = stackify.rum.insert_rum_script() assert not result self.reset_common_config() - self.restore_apm_installed() + @mock.patch('stackify.rum.is_apm_installed') @mock.patch('stackify.rum.get_reporting_url') @mock.patch('stackify.rum.get_transaction_id') - def test_default_insert_rum_script_from_api(self, func, func_reporting_url): + def test_default_insert_rum_script_from_api(self, func, func_reporting_url, func_apm): func.return_value = '123' + func_apm.return_value = False func_reporting_url.return_value = 'test reporting url' - self.update_apm_installed(False) self.create_config( rum_key='asd1', application='app1', @@ -153,11 +121,11 @@ def test_default_insert_rum_script_from_api(self, func, func_reporting_url): } result = stackify.rum.insert_rum_script() self.reset_common_config() - self.restore_apm_installed() assert result == ''.format(json.dumps(rum_settings)) - def test_default_insert_rum_script_no_key_from_api(self): - self.update_apm_installed(False) + @mock.patch('stackify.rum.is_apm_installed') + def test_default_insert_rum_script_no_key_from_api(self, func_apm): + func_apm.return_value = False self.create_config( rum_key=None, application='app2', @@ -166,12 +134,12 @@ def test_default_insert_rum_script_no_key_from_api(self): result = stackify.rum.insert_rum_script() self.reset_common_config() - self.restore_apm_installed() assert not result - def test_default_insert_rum_script_no_details_from_api(self): - self.update_apm_installed(False) + @mock.patch('stackify.rum.is_apm_installed') + def test_default_insert_rum_script_no_details_from_api(self, func_apm): + func_apm.return_value = False self.create_config( application=None, environment=None, @@ -180,17 +148,9 @@ def test_default_insert_rum_script_no_details_from_api(self): result = stackify.rum.insert_rum_script() self.reset_common_config() - self.restore_apm_installed() assert not result - def update_apm_installed(self, installed): - self.apm_installed = stackify.rum.apm_installed - stackify.rum.apm_installed = installed - - def restore_apm_installed(self): - stackify.rum.apm_installed = self.apm_installed - def update_common_config(self, rum_key=None, rum_script_url=None, application=None, environment=None): self.config_rum_key = stackify.config.rum_key self.config_rum_script_url = stackify.config.rum_script_url From b988b641c16346ac8fafa2af3b8f58b5826211c6 Mon Sep 17 00:00:00 2001 From: Michael Mantos Date: Tue, 3 Aug 2021 20:46:41 +0800 Subject: [PATCH 3/4] LNX-329 - Change None to empty space inject rum script --- stackify/rum.py | 16 ++++++++-------- tests/rum/test_rum_apm.py | 1 + tests/test_rum.py | 7 ++++++- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/stackify/rum.py b/stackify/rum.py index 50350df..10f2f76 100644 --- a/stackify/rum.py +++ b/stackify/rum.py @@ -13,30 +13,30 @@ def insert_rum_script(): apm_rum_script = insert_rum_script_apm() - if apm_rum_script: + if apm_rum_script is not None: return apm_rum_script rum_key = config.rum_key rum_script_url = config.rum_script_url if not rum_script_url or not rum_key: - return None + return '' transaction_id = get_transaction_id() if not transaction_id: - return None + return '' reporting_url = get_reporting_url() if not reporting_url: - return None + return '' application_name = config.application if not application_name: - return None + return '' environment = config.environment if not environment: - return None + return '' settings = { "ID": transaction_id @@ -58,7 +58,7 @@ def insert_rum_script(): settings["Trans"] = reporting_url_b64 if not settings: - return None + return '' return ''.format( json.dumps(settings), @@ -77,7 +77,7 @@ def get_reporting_url(): def insert_rum_script_apm(): if not is_apm_installed(): - return + return None return insert_rum_script_from_apm() diff --git a/tests/rum/test_rum_apm.py b/tests/rum/test_rum_apm.py index fc8a28d..0a9a699 100644 --- a/tests/rum/test_rum_apm.py +++ b/tests/rum/test_rum_apm.py @@ -74,3 +74,4 @@ def test_default_insert_rum_script_from_apm_without_transaction(self): rum_data = stackify.rum.insert_rum_script() assert not rum_data + assert rum_data is '' diff --git a/tests/test_rum.py b/tests/test_rum.py index 17fc683..b1e77fb 100644 --- a/tests/test_rum.py +++ b/tests/test_rum.py @@ -75,7 +75,8 @@ def test_default_insert_rum_script_no_transaction_id(self, func_apm): result = stackify.rum.insert_rum_script() self.reset_common_config() - assert result is None + assert not result + assert result is '' @mock.patch('stackify.rum.is_apm_installed') def test_default_insert_rum_script_no_key(self, func_apm): @@ -88,6 +89,7 @@ def test_default_insert_rum_script_no_key(self, func_apm): result = stackify.rum.insert_rum_script() assert not result + assert result is '' self.reset_common_config() @@ -98,6 +100,7 @@ def test_default_insert_rum_script_no_details(self, func_apm): result = stackify.rum.insert_rum_script() assert not result + assert result is '' self.reset_common_config() @@ -136,6 +139,7 @@ def test_default_insert_rum_script_no_key_from_api(self, func_apm): self.reset_common_config() assert not result + assert result is '' @mock.patch('stackify.rum.is_apm_installed') def test_default_insert_rum_script_no_details_from_api(self, func_apm): @@ -150,6 +154,7 @@ def test_default_insert_rum_script_no_details_from_api(self, func_apm): self.reset_common_config() assert not result + assert result is '' def update_common_config(self, rum_key=None, rum_script_url=None, application=None, environment=None): self.config_rum_key = stackify.config.rum_key From dbf41456280341022162e15dfbe37fb13df9abc1 Mon Sep 17 00:00:00 2001 From: Michael Mantos Date: Fri, 13 Aug 2021 02:43:48 +0800 Subject: [PATCH 4/4] LNX-329 - Add RUMv2 doc --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index ba7de4c..81a457d 100644 --- a/README.md +++ b/README.md @@ -123,3 +123,22 @@ logger = logging.getLogger('django') logger.warning('Something happened') ``` + + +## **Real User Monitoring (RUM)** + +Real user monitoring injects a script tag containing the [RUM JS](https://stackify.com/retrace-real-user-monitoring/) that is responsible for capturing information about the http requests on the browser. This approach is manual and needs to be configured. + +### RUM - Setup + +```python +# Configuration - Standard API +logger = stackify.getLogger(..., rum_key="YourRumKey") +# or Configuration - Python Logging Integration +stackify.StackifyHandler(..., rum_key="YourRumKey") + +# Use this to apply on views +import stackify.rum + +stackify.rum.insert_rum_script() +``` \ No newline at end of file