Skip to content

Commit eecc211

Browse files
authored
Merge pull request #13 from homiedopie/feature/LNX-329-rum-v2
LNX-329 - Initial commit for RUMv2 support
2 parents 1884d76 + dbf4145 commit eecc211

File tree

16 files changed

+663
-2
lines changed

16 files changed

+663
-2
lines changed

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,22 @@ logger = logging.getLogger('django')
123123

124124
logger.warning('Something happened')
125125
```
126+
127+
128+
## **Real User Monitoring (RUM)**
129+
130+
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.
131+
132+
### RUM - Setup
133+
134+
```python
135+
# Configuration - Standard API
136+
logger = stackify.getLogger(..., rum_key="YourRumKey")
137+
# or Configuration - Python Logging Integration
138+
stackify.StackifyHandler(..., rum_key="YourRumKey")
139+
140+
# Use this to apply on views
141+
import stackify.rum
142+
143+
stackify.rum.insert_rum_script()
144+
```

docker/stackify-python-api-test

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ ARG from_version
33
FROM python:${from_version}
44

55
ARG version
6+
ARG test
7+
ARG test_repo
68

79
RUN \
810
apt-get update && \
@@ -14,4 +16,7 @@ COPY . /build/
1416

1517
RUN cat /build/requirements.txt | xargs -n 1 pip install; exit 0
1618

19+
ENV TEST="${test}"
20+
ENV TEST_REPO="${test_repo}"
21+
1722
CMD /bin/bash -c "cd /build && source test-docker-execute.sh"

stackify/compat.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# -*- coding: utf-8 -*-
2+
import sys
3+
import types
4+
5+
PY2 = sys.version_info[0] == 2
6+
PY3 = sys.version_info[0] == 3
7+
8+
9+
if PY2:
10+
import StringIO
11+
import Queue as queue # noqa F401
12+
import urlparse # noqa F401
13+
from urllib2 import HTTPError # noqa F401
14+
from urllib import unquote as unquote_core # noqa F401
15+
16+
StringIO = BytesIO = StringIO.StringIO
17+
18+
string_types = (basestring,) # noqa F821
19+
integer_types = (int, long) # noqa F821
20+
class_types = (type, types.ClassType)
21+
text_type = unicode # noqa F821
22+
binary_type = str
23+
list_type = list
24+
dict_type = dict
25+
26+
def b(s):
27+
return s
28+
29+
def iterkeys(d, **kwargs):
30+
return d.iterkeys(**kwargs)
31+
32+
def iteritems(d, **kwargs):
33+
return d.iteritems(**kwargs)
34+
35+
def iterlists(d, **kwargs):
36+
return d.iterlists(**kwargs)
37+
38+
def unquote(*args, **kwargs): # noqa F811
39+
return unquote_core(*args, **kwargs)
40+
else:
41+
import io
42+
import queue # noqa F401
43+
from urllib import parse as urlparse # noqa F401
44+
from urllib.error import HTTPError # noqa F401
45+
46+
StringIO = io.StringIO
47+
BytesIO = io.BytesIO
48+
49+
string_types = (str,)
50+
integer_types = (int,)
51+
class_types = (type,)
52+
text_type = str
53+
binary_type = bytes
54+
list_type = list
55+
dict_type = dict
56+
57+
def b(s):
58+
return s.encode("latin-1")
59+
60+
def iterkeys(d, **kwargs):
61+
return iter(d.keys(**kwargs))
62+
63+
def iteritems(d, **kwargs):
64+
return iter(d.items(**kwargs))
65+
66+
def iterlists(d, **kwargs):
67+
return iter(d.lists(**kwargs))
68+
69+
def unquote(*args, **kwargs):
70+
return urlparse.unquote(*args, **kwargs)
71+
72+
73+
def multidict_to_dict(d):
74+
return dict((k, v[0] if len(v) == 1 else v) for k, v in iterlists(d))

stackify/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from stackify.constants import DEFAULT_RUM_KEY, DEFAULT_RUM_SCRIPT_URL
2+
3+
rum_key = DEFAULT_RUM_KEY
4+
rum_script_url = DEFAULT_RUM_SCRIPT_URL
5+
application = None
6+
environment = None

stackify/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,6 @@
3838
TRANSPORT_TYPE_DEFAULT = 'default'
3939
TRANSPORT_TYPE_AGENT_SOCKET = 'agent_socket'
4040
TRANSPORT_TYPE_AGENT_HTTP = 'agent_http'
41+
42+
DEFAULT_RUM_SCRIPT_URL = "https://stckjs.stackify.com/stckjs.js"
43+
DEFAULT_RUM_KEY = ""

stackify/rum.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import json
2+
import base64
3+
from stackify import config
4+
5+
apm_installed = False
6+
7+
try:
8+
from stackifyapm import insert_rum_script as insert_rum_script_from_apm
9+
apm_installed = True
10+
except (ImportError):
11+
pass
12+
13+
14+
def insert_rum_script():
15+
apm_rum_script = insert_rum_script_apm()
16+
if apm_rum_script is not None:
17+
return apm_rum_script
18+
19+
rum_key = config.rum_key
20+
rum_script_url = config.rum_script_url
21+
22+
if not rum_script_url or not rum_key:
23+
return ''
24+
25+
transaction_id = get_transaction_id()
26+
if not transaction_id:
27+
return ''
28+
29+
reporting_url = get_reporting_url()
30+
if not reporting_url:
31+
return ''
32+
33+
application_name = config.application
34+
if not application_name:
35+
return ''
36+
37+
environment = config.environment
38+
if not environment:
39+
return ''
40+
41+
settings = {
42+
"ID": transaction_id
43+
}
44+
45+
if application_name:
46+
application_name_b64 = base64.b64encode(application_name.encode("utf-8")).decode("utf-8")
47+
if (application_name_b64):
48+
settings["Name"] = application_name_b64
49+
50+
if environment:
51+
environment_b64 = base64.b64encode(environment.encode("utf-8")).decode("utf-8")
52+
if (environment_b64):
53+
settings["Env"] = environment_b64
54+
55+
if reporting_url:
56+
reporting_url_b64 = base64.b64encode(reporting_url.encode("utf-8")).decode("utf-8")
57+
if (reporting_url_b64):
58+
settings["Trans"] = reporting_url_b64
59+
60+
if not settings:
61+
return ''
62+
63+
return '<script type="text/javascript">(window.StackifySettings || (window.StackifySettings = {}))</script><script src="{}" data-key="{}" async></script>'.format(
64+
json.dumps(settings),
65+
rum_script_url,
66+
rum_key
67+
)
68+
69+
70+
def get_transaction_id():
71+
return ''
72+
73+
74+
def get_reporting_url():
75+
return ''
76+
77+
78+
def insert_rum_script_apm():
79+
if not is_apm_installed():
80+
return None
81+
82+
return insert_rum_script_from_apm()
83+
84+
85+
def is_apm_installed():
86+
return apm_installed

stackify/transport/application.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import socket
22
import os
3+
import logging
34

45
from stackify.utils import arg_or_env
56
from stackify.constants import API_URL
@@ -9,6 +10,12 @@
910
from stackify.constants import TRANSPORT_TYPE_AGENT_SOCKET
1011
from stackify.constants import TRANSPORT_TYPE_DEFAULT
1112
from stackify.transport.default.formats import JSONObject
13+
from stackify.constants import DEFAULT_RUM_SCRIPT_URL
14+
from stackify.constants import DEFAULT_RUM_KEY
15+
from stackify.utils import RegexValidator, ConfigError
16+
from stackify import config
17+
18+
internal_logger = logging.getLogger(__name__)
1219

1320

1421
class EnvironmentDetail(JSONObject):
@@ -38,6 +45,8 @@ def __init__(
3845
socket_url=SOCKET_URL,
3946
transport=None,
4047
http_endpoint=DEFAULT_HTTP_ENDPOINT,
48+
rum_script_url=DEFAULT_RUM_SCRIPT_URL,
49+
rum_key=DEFAULT_RUM_KEY
4150
):
4251
self.api_key = api_key
4352
self.api_url = api_url
@@ -47,6 +56,39 @@ def __init__(
4756
self.http_endpoint = http_endpoint
4857
self.transport = transport
4958

59+
self.rum_script_url = DEFAULT_RUM_SCRIPT_URL
60+
self.rum_key = DEFAULT_RUM_KEY
61+
62+
# Rum config validation
63+
if rum_script_url != DEFAULT_RUM_SCRIPT_URL:
64+
self.validate(
65+
RegexValidator("^((((https?|ftps?|gopher|telnet|nntp)://)|(mailto:|news:))(%[0-9A-Fa-f]{2}|[-\(\)_.!~*';/?:@&=+$,A-Za-z0-9])+)([).!';/?:,][\[:blank:|:blank:\]])?$"),
66+
rum_script_url,
67+
'rum_script_url'
68+
)
69+
config.rum_script_url = self.rum_script_url
70+
71+
if rum_key != DEFAULT_RUM_KEY:
72+
self.validate(
73+
RegexValidator("^[A-Za-z0-9_-]+$"),
74+
rum_key,
75+
'rum_key'
76+
)
77+
config.rum_key = self.rum_key
78+
79+
config.environment = self.environment
80+
config.application = self.application
81+
82+
def validate(self, validator, value, key):
83+
if not validator:
84+
return
85+
86+
try:
87+
value = validator(value, key)
88+
setattr(self, key, str(value))
89+
except ConfigError as e:
90+
internal_logger.exception(str(e))
91+
5092

5193
def get_configuration(**kwargs):
5294
"""
@@ -69,4 +111,6 @@ def get_configuration(**kwargs):
69111
socket_url=arg_or_env('socket_url', kwargs, SOCKET_URL),
70112
http_endpoint=arg_or_env('http_endpoint', kwargs, DEFAULT_HTTP_ENDPOINT, env_key='STACKIFY_TRANSPORT_HTTP_ENDPOINT'),
71113
transport=transport,
114+
rum_script_url=arg_or_env('rum_script_url', kwargs, DEFAULT_RUM_SCRIPT_URL, env_key='RETRACE_RUM_SCRIPT_URL'),
115+
rum_key=arg_or_env('rum_key', kwargs, DEFAULT_RUM_KEY, env_key='RETRACE_RUM_KEY')
72116
)

stackify/utils.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import os
22
import json
33
import logging
4+
import re
5+
from stackify import compat
46

57
internal_logger = logging.getLogger(__name__)
68

@@ -36,3 +38,22 @@ def get_default_object(obj):
3638

3739
def object_is_iterable(obj):
3840
return hasattr(obj, '__iter__') or isinstance(obj, str)
41+
42+
43+
class RegexValidator(object):
44+
def __init__(self, regex, verbose_pattern=None):
45+
self.regex = regex
46+
self.verbose_pattern = verbose_pattern or regex
47+
48+
def __call__(self, value, field_name):
49+
value = compat.text_type(value)
50+
match = re.match(self.regex, value)
51+
if match:
52+
return value
53+
raise ConfigError("{} does not match pattern {}".format(value, self.verbose_pattern), field_name)
54+
55+
56+
class ConfigError(ValueError):
57+
def __init__(self, msg, field_name):
58+
self.field_name = field_name
59+
super(ValueError, self).__init__(msg)

test-docker-execute.sh

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,16 @@ function runPyTest() {
1414
echo '<--------------------------------------------->'
1515
echo "Python Version $(python --version)"
1616
echo 'Running pytest...'
17-
py.test
17+
py.test --ignore=tests/rum
18+
19+
if [ "${TEST}" = 1 ]; then
20+
pip install -i "${TEST_REPO}" stackify-python-apm;
21+
else
22+
pip install stackify-python-apm;
23+
py.test tests/rum
24+
fi
25+
26+
pip uninstall -y stackify-python-apm
1827
}
1928

2029
runFlake8

test-docker.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ do
1818
fi
1919

2020
echo "Building stackify-python-api-test-${i}..."
21-
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
21+
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
2222

2323
echo "Running stackify-python-api-test-${i}..."
2424
docker run --network="host" --name "stackify-python-api-test-${i}" stackify-python-api-test-${i}:latest

0 commit comments

Comments
 (0)