diff --git a/.gitignore b/.gitignore
index 1ae3595..2689372 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,32 +1,112 @@
-*.py[co]
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
-# Packages
-*.egg
-*.egg-info
-dist
-build
-eggs
-parts
-var
-sdist
-develop-eggs
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
# Installer logs
pip-log.txt
+pip-delete-this-directory.txt
# Unit test / coverage reports
+htmlcov/
+.tox/
.coverage
-.tox
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+.pytest_cache/
-#Translations
+# Translations
*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
-#Mr Developer
-.mr.developer.cfg
+# Jupyter Notebook
+.ipynb_checkpoints
-# virutalenvs
+# pyenv
+.python-version
+
+# celery beat schedule file
+celerybeat-schedule
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
.venv
+env/
+venv/
+env*/
+venv*/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+
+# Intellij
+.idea
-# debug stuff
-test.py
+# Protobuf
+*.proto
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..b10266c
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,62 @@
+Stackify Python API LICENSE
+---
+
+Certain inventions disclosed in this file may be claimed within patents owned or patent applications filed by
+Stackify, LLC (“Stackify”) or third parties.
+
+Stackify grants you a revocable, non-exclusive, non-transferable, limited license to download, install and use this
+Stackify Python API package (“Application”) strictly in accordance with the terms of this Agreement and the terms found
+at https://stackify.com/terms-conditions.
+
+You agree not to, and you will not permit others to: license, sell, rent, lease, assign, distribute, transmit, host,
+outsource, disclose or otherwise commercially exploit the Application or make the Application available to any third
+party; modify, make derivative works of, disassemble, decrypt, reverse compile or reverse engineer any part of the
+Application; or remove, alter or obscure any proprietary notice (including any notice of copyright or trademark) of
+Stackify or its affiliates, partners, suppliers or the licensors of the Application.
+
+You may install, execute, and distribute these files and their contents only in conjunction with your direct use of
+Stackify’s services. These files and their contents shall not be used in conjunction with any other product or
+software, including but not limited to those that may compete with any Stackify product, feature, or software. As a
+condition to the foregoing grant, you must provide this notice along with each copy you distribute and you must not
+remove, alter, or obscure this notice.
+
+The Application, including without limitation all copyrights, patents, trademarks, trade secrets and other intellectual
+property rights are, and shall remain, the sole and exclusive property of Stackify. Any feedback, comments, ideas,
+code, pull requests, improvements or suggestions (collectively, "Suggestions") provided by you to Stackify with respect
+to the Application shall remain the sole and exclusive property of Stackify. Stackify shall be free to use, copy,
+modify, publish, or redistribute the Suggestions for any purpose and in any way without any credit or any compensation
+to you.
+
+Stackify reserves the right to modify, suspend or discontinue, temporarily or permanently, the Application or any
+service to which it connects, with or without notice and without liability to you.
+
+Stackify may from time to time provide enhancements or improvements to the features/functionality of the Application,
+which may include patches, bug fixes, updates, upgrades and other modifications ("Updates"). Updates may modify or
+delete certain features and/or functionalities of the Application. You agree that Stackify has no obligation to (i)
+provide any Updates, or (ii) continue to provide or enable any particular features and/or functionalities of the
+Application to you. You further agree that all Updates will be (i) deemed to constitute an integral part of the
+Application, and (ii) subject to the terms and conditions of this Agreement.
+
+All other use, reproduction, modification, distribution, or other exploitation of these files is strictly prohibited,
+except as may be set forth in a separate written license agreement between you and Stackify. The terms of any such
+license agreement will control over this notice. The license stated above will be automatically terminated and
+revoked if you exceed its scope or violate any of the terms of this notice.
+
+Upon termination of this Agreement, you shall cease all use of the Application and delete all copies of the
+Application from your mobile device or from your computer.
+
+Termination of this Agreement will not limit any of Stackify LLC's rights or remedies at law or in equity in case of
+breach by you (during the term of this Agreement) of any of your obligations under the present Agreement.
+
+You agree to indemnify and hold Stackify LLC and its parents, subsidiaries, affiliates, officers, employees, agents,
+partners and licensors (if any) harmless from any claim or demand, including reasonable attorneys' fees, due to or
+arising out of your: (a) use of the Application; (b) violation of this Agreement or any law or regulation; or (c)
+violation of any right of a third party.
+
+Unless otherwise expressly agreed by Stackify in a separate written license agreement, these files are provided
+AS IS, WITHOUT WARRANTY OF ANY KIND, including without any implied warranties of MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE, TITLE, or NON-INFRINGEMENT. As a condition to your use of these files, you are solely responsible
+for such use. Stackify will have no liability to you for direct, indirect, consequential, incidental, special, or
+punitive damages or for lost profits or data.
+
+
diff --git a/LICENSE.txt b/LICENSE.txt
deleted file mode 100644
index 1333ed7..0000000
--- a/LICENSE.txt
+++ /dev/null
@@ -1 +0,0 @@
-TODO
diff --git a/README.md b/README.md
index f29f16a..81a457d 100644
--- a/README.md
+++ b/README.md
@@ -1,57 +1,54 @@
Stackify API for Python
-=======
-
-[Stackify](https://stackify.com) support for Python programs.
-
-```python
-import stackify
-
-logger = stackify.getLogger()
-
-try:
- "Make it so, #" + 1
-except:
- logger.exception("Can't add strings and numbers")
-```
+=======================
## Installation
stackify-python can be installed through pip:
```bash
-$ pip install -U stackify
+$ pip install -U stackify-api-python
```
-You can also check out the repository and install with setuptools:
+**stackify-python-api** can be installed through pip:
```bash
-$ ./setup.py install
+$ pip install stackify-api-python
```
## Configuration
-Your Stackify setup information can be provided via environment variables. For example:
-```bash
-export STACKIFY_APPLICATION=MyApp
-export STACKIFY_ENVIRONMENT=Dev
-export STACKIFY_API_KEY=******
-```
-These options can also be provided in your code:
+
+#### Standard API
```python
import stackify
+logger = stackify.getLogger(application="Python Application", environment="Production", api_key="***")
+logger.warning('Something happened')
+```
+
+#### Python Logging Integration
-logger = stackify.getLogger(application="MyApp", environment="Dev", api_key=******)
+```python
+import logging
+import stackify
+logger = logging.getLogger(__name__)
+stackify_handler = stackify.StackifyHandler(application="Python Application", environment="Production", api_key="***")
+logger.addHandler(stackify_handler)
logger.warning('Something happened')
```
+#### Environment Settings
+
+```bash
+export STACKIFY_APPLICATION=Python Application
+export STACKIFY_ENVIRONMENT=Production
+export STACKIFY_API_KEY=******
+```
+
+
## Usage
-stackify-python handles uploads in batches of 100 messages at a time on another thread.
+**stackify-python-api** handles uploads in batches of 100 messages at a time on another thread.
When your program exits, it will shut the thread down and upload the remaining messages.
Stackify can store extra data along with your log message:
```python
-import stackify
-
-logger = stackify.getLogger()
-
try:
user_string = raw_input("Enter a number: ")
print("You entered", int(user_string))
@@ -62,7 +59,6 @@ except ValueError:
You can also name your logger instead of using the automatically generated one:
```python
import stackify
-
logger = stackify.getLogger('mymodule.myfile')
```
@@ -72,8 +68,9 @@ This library has an internal logger it uses for debugging and messaging.
For example, if you want to enable debug messages:
```python
import logging
-
-logging.getLogger('stackify').setLevel(logging.DEBUG)
+logger = logging.getLogger('stackify')
+logger.setLevel(logging.DEBUG)
+logger.addHandler(logging.FileHandler('stackify.log')) # or any handler you want
```
By default, it will enable the default logging settings via `logging.basicConfig()`
@@ -85,15 +82,63 @@ import stackify
logger = stackify.getLogger(basic_config=False)
```
-## Testing
-Run the test suite with setuptools:
-```bash
-$ ./setup.py test
+## Django Logging Integration
+
+You can also use your existing django logging and just append stackify logging handler
+
+```python
+LOGGING = {
+ 'version': 1,
+ 'disable_existing_loggers': False,
+ 'handlers': {
+ 'file': {
+ 'level': 'DEBUG',
+ 'class': 'logging.FileHandler',
+ 'filename': 'debug.log',
+ },
+ 'stackify': {
+ 'level': 'DEBUG',
+ 'class': 'stackify.StackifyHandler',
+ 'application': 'MyApp',
+ 'environment': 'Dev',
+ 'api_key': '******',
+ }
+ },
+ 'loggers': {
+ 'django': {
+ 'handlers': ['file', 'stackify'],
+ 'level': 'DEBUG',
+ 'propagate': True,
+ },
+ },
+}
```
-You can obtain a coverage report with nose:
-```bash
-$ ./setup nosetests --with-coverage --cover-package=stackify
+Usage
+```python
+import logging
+
+logger = logging.getLogger('django')
+
+
+logger.warning('Something happened')
```
-You might need to install the `nose` and `coverage` packages.
+
+## **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
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
new file mode 100644
index 0000000..1b16f89
--- /dev/null
+++ b/azure-pipelines.yml
@@ -0,0 +1,61 @@
+# Python package
+# Create and test a Python package on multiple Python versions.
+# Add steps that analyze code, save the dist with the build record, publish to a PyPI-compatible index, and more:
+# https://docs.microsoft.com/azure/devops/pipelines/languages/python
+
+trigger:
+- master
+
+schedules:
+- cron: "0 21-22 * * 0"
+ displayName: Weekly build
+ branches:
+ include:
+ - master
+ always: true
+
+pool:
+ vmImage: ubuntu-latest
+strategy:
+ matrix:
+# Python27:
+# python.version: '2.7'
+# Python35:
+# python.version: '3.5'
+# Python36:
+# python.version: '3.6'
+ Python37:
+ python.version: '3.7'
+
+steps:
+- task: UsePythonVersion@0
+ inputs:
+ versionSpec: '$(python.version)'
+ displayName: 'Use Python $(python.version)'
+
+- script: |
+ python -m pip install --upgrade pip
+ pip install -r requirements.txt
+ displayName: 'Install dependencies'
+
+- task: CmdLine@2
+ inputs:
+ script: 'curl -sSL https://www.sourceclear.com/install | sh'
+- task: CmdLine@2
+ inputs:
+ script: 'srcclr scan .'
+ env:
+ SRCCLR_API_TOKEN: $(SRCCLR_API_TOKEN)
+
+- task: CmdLine@2
+ inputs:
+ script: 'zip -r stackify-api-python.zip stackify/ requirements.txt setup.py'
+- task: Veracode@3
+ inputs:
+ ConnectionDetailsSelection: 'Endpoint'
+ AnalysisService: 'Veracode'
+ veracodeAppProfile: 'Retrace Python Library'
+ version: 'AZ-Devops-Build-$(build.buildNumber)'
+ filepath: 'stackify-api-python.zip'
+ maximumWaitTime: '360'
+ optargs: -deleteincompletescan 2
diff --git a/docker/stackify-python-api-test b/docker/stackify-python-api-test
new file mode 100644
index 0000000..7383754
--- /dev/null
+++ b/docker/stackify-python-api-test
@@ -0,0 +1,22 @@
+ARG from_version
+
+FROM python:${from_version}
+
+ARG version
+ARG test
+ARG test_repo
+
+RUN \
+ apt-get update && \
+ pip install --upgrade pip && \
+ python --version
+
+RUN mkdir /build
+COPY . /build/
+
+RUN cat /build/requirements.txt | xargs -n 1 pip install; exit 0
+
+ENV TEST="${test}"
+ENV TEST_REPO="${test_repo}"
+
+CMD /bin/bash -c "cd /build && source test-docker-execute.sh"
diff --git a/docs/Developer.md b/docs/Developer.md
new file mode 100644
index 0000000..cbc6905
--- /dev/null
+++ b/docs/Developer.md
@@ -0,0 +1,11 @@
+## Testing
+Run the test suite with setuptools:
+```bash
+$ ./setup.py test
+```
+
+You can obtain a coverage report with nose:
+```bash
+$ ./setup nosetests --with-coverage --cover-package=stackify
+```
+You might need to install the `nose` and `coverage` packages.
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..811c4e4
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,8 @@
+flake8
+mock==2.0.0
+protobuf==3.15.0
+pytest==4.3.0
+pytest-cov==2.6.1
+requests==2.21.0
+requests-unixsocket==0.2.0
+retrying==1.3.3
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..470d4da
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,26 @@
+[flake8]
+ignore = E501, W605
+exclude =
+ .git,
+ __pycache__,
+ build,
+ dist,
+ env*,
+ venv*,
+ setup.cfg,
+ README.md,
+ LICENSE.md,
+ requirements.txt,
+ *protos*,
+
+
+[coverage:run]
+include =
+ stackify/*
+omit =
+ *tests*,
+ *handler_backport.py,
+ *protos*,
+
+[tool:pytest]
+python_files=tests.py test.py test_*.py *_test.py tests_*.py *_tests.py
diff --git a/setup.py b/setup.py
index d19d42b..05cfeab 100755
--- a/setup.py
+++ b/setup.py
@@ -1,14 +1,17 @@
#!/usr/bin/env python
-from setuptools import setup
+import setuptools
import re
import ast
+with open("README.md", "r") as fh:
+ long_description = fh.read()
+
try:
from pypandoc import convert
- read_md = lambda f: convert(f, 'rst')
+ read_md = lambda f: convert(f, 'rst') # noqa
except ImportError:
print('warning: pypandoc module not found, could not convert Markdown to RST')
- read_md = lambda f: open(f).read()
+ read_md = lambda f: open(f).read() # noqa
version_re = re.compile(r'__version__\s+=\s+(.*)')
@@ -16,27 +19,38 @@
f = f.read()
version = ast.literal_eval(version_re.search(f).group(1))
-setup(
- name = 'stackify',
- version = version,
- author = 'Matthew Thompson',
- author_email = 'chameleonator@gmail.com',
- packages = ['stackify'],
- url = 'https://github.com/stackify/stackify-api-python',
- license = open('LICENSE.txt').readline(),
- description = 'Stackify API for Python',
- long_description = read_md('README.md'),
- download_url = 'https://github.com/stackify/stackify-api-python/tarball/0.0.1',
- keywords = ['logging', 'stackify', 'exception'],
- classifiers=["Programming Language :: Python"],
- install_requires = [
+setuptools.setup(
+ name='stackify-api-python',
+ version=version,
+ author='Stackify',
+ author_email='support@stackify.com',
+ packages=setuptools.find_packages(exclude=("tests", "*tests", "tests*",)),
+ url='https://github.com/stackify/stackify-api-python',
+ description='Stackify API for Python',
+ long_description=long_description,
+ long_description_content_type="text/markdown",
+ keywords=['logging', 'stackify', 'exception'],
+ classifiers=[
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 2.7",
+ "Programming Language :: Python :: 3.4",
+ "Programming Language :: Python :: 3.5",
+ "Programming Language :: Python :: 3.6",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: Implementation :: CPython",
+ "Programming Language :: Python :: Implementation :: PyPy",
+ "Operating System :: OS Independent",
+ ],
+ install_requires=[
+ 'protobuf>=3.9.1',
'retrying>=1.2.3',
- 'requests>=2.4.1'
+ 'requests>=2.4.1',
+ 'requests-unixsocket>=0.2.0'
],
- test_suite = 'tests',
- tests_requires = [
+ test_suite='tests',
+ tests_requires=[
'mock>=1.0.1',
'nose==1.3.4'
]
)
-
diff --git a/stackify/__init__.py b/stackify/__init__.py
index f276a43..1564d5e 100644
--- a/stackify/__init__.py
+++ b/stackify/__init__.py
@@ -1,45 +1,25 @@
"""
Stackify Python API
"""
-
-__version__ = '0.0.1'
-
-
-API_URL = 'https://api.stackify.com'
-
-READ_TIMEOUT = 5000
-
-MAX_BATCH = 100
-
-QUEUE_SIZE = 1000
+__version__ = '1.2.0'
import logging
import inspect
import atexit
-DEFAULT_LEVEL = logging.ERROR
-
-LOGGING_LEVELS = {
- logging.CRITICAL: 'CRITICAL',
- logging.ERROR: 'ERROR',
- logging.WARNING: 'WARNING',
- logging.INFO: 'INFO',
- logging.DEBUG: 'DEBUG',
- logging.NOTSET: 'NOTSET'
-}
+from stackify.constants import DEFAULT_LEVEL
+from stackify.handler import StackifyHandler
class NullHandler(logging.Handler):
def emit(self, record):
pass
-logging.getLogger(__name__).addHandler(NullHandler())
-
-from stackify.application import ApiConfiguration
-from stackify.http import HTTPClient
-
-from stackify.handler import StackifyHandler
+internal_logger = logging.getLogger(__name__)
+internal_logger.addHandler(NullHandler())
+internal_logger.propagate = False
+internal_logger.setLevel(DEFAULT_LEVEL)
def getLogger(name=None, auto_shutdown=True, basic_config=True, **kwargs):
@@ -74,11 +54,8 @@ def getLogger(name=None, auto_shutdown=True, basic_config=True, **kwargs):
logger = logging.getLogger(name)
- if not [isinstance(x, StackifyHandler) for x in logger.handlers]:
- internal_logger = logging.getLogger(__name__)
+ if not any([isinstance(x, StackifyHandler) for x in logger.handlers]):
internal_logger.debug('Creating handler for logger %s', name)
- handler = StackifyHandler(**kwargs)
- logger.addHandler(handler)
if auto_shutdown:
internal_logger.debug('Registering atexit callback')
@@ -87,7 +64,8 @@ def getLogger(name=None, auto_shutdown=True, basic_config=True, **kwargs):
if logger.getEffectiveLevel() == logging.NOTSET:
logger.setLevel(DEFAULT_LEVEL)
- handler.listener.start()
+ handler = StackifyHandler(ensure_at_exit=not auto_shutdown, **kwargs)
+ logger.addHandler(handler)
return logger
@@ -98,7 +76,7 @@ def stopLogging(logger):
Shut down the StackifyHandler on a given logger. This will block
and wait for the queue to finish uploading.
'''
- internal_logger = logging.getLogger(__name__)
+
internal_logger.debug('Shutting down all handlers')
for handler in getHandlers(logger):
handler.listener.stop()
diff --git a/stackify/application.py b/stackify/application.py
deleted file mode 100644
index 69e8e71..0000000
--- a/stackify/application.py
+++ /dev/null
@@ -1,44 +0,0 @@
-import socket
-import os
-
-from stackify import API_URL
-from stackify.formats import JSONObject
-
-
-class EnvironmentDetail(JSONObject):
- def __init__(self, api_config):
- self.deviceName = socket.gethostname()
- self.appLocation = os.getcwd()
- self.configuredAppName = api_config.application
- self.configuredEnvironmentName = api_config.environment
-
-
-class ApiConfiguration:
- def __init__(self, api_key, application, environment, api_url=API_URL):
- self.api_key = api_key
- self.api_url = api_url
- self.application = application
- self.environment = environment
-
-
-def arg_or_env(name, args, default=None):
- env_name = 'STACKIFY_{0}'.format(name.upper())
- try:
- value = args.get(name)
- if not value:
- value = os.environ[env_name]
- return value
- except KeyError:
- if default:
- return default
- else:
- raise NameError('You must specify the keyword argument {0} or '
- 'environment variable {1}'.format(name, env_name))
-
-
-def get_configuration(**kwargs):
- return ApiConfiguration(
- application=arg_or_env('application', kwargs),
- environment=arg_or_env('environment', kwargs),
- api_key=arg_or_env('api_key', kwargs),
- api_url=arg_or_env('api_url', kwargs, API_URL))
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
new file mode 100644
index 0000000..c560063
--- /dev/null
+++ b/stackify/constants.py
@@ -0,0 +1,43 @@
+import logging
+
+
+API_URL = 'https://api.stackify.com'
+IDENTIFY_URL = '/Metrics/IdentifyApp'
+LOG_SAVE_URL = '/Log/Save'
+
+# using `%2F` instead of `/` as per package documentation
+DEFAULT_SOCKET_FILE = '%2Fusr%2Flocal%2Fstackify%2Fstackify.sock'
+DEFAULT_HTTP_ENDPOINT = 'https://localhost:10601'
+SOCKET_URL = 'http+unix://' + DEFAULT_SOCKET_FILE
+AGENT_LOG_URL = '/log'
+
+API_REQUEST_INTERVAL_IN_SEC = 30
+
+MAX_BATCH = 100
+QUEUE_SIZE = 1000
+READ_TIMEOUT = 5000
+
+LOGGING_LEVELS = {
+ logging.CRITICAL: 'CRITICAL',
+ logging.ERROR: 'ERROR',
+ logging.WARNING: 'WARNING',
+ logging.INFO: 'INFO',
+ logging.DEBUG: 'DEBUG',
+ logging.NOTSET: 'NOTSET'
+}
+DEFAULT_LEVEL = logging.INFO
+
+# this is used to separate builtin keys from user-specified keys
+RECORD_VARS = set(logging.LogRecord('', '', '', '', '', '', '', '').__dict__.keys())
+
+# the "message" attribute is saved on the record object by a Formatter
+RECORD_VARS.add('message')
+RECORD_VARS.add('trans_id')
+RECORD_VARS.add('log_id')
+
+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/handler.py b/stackify/handler.py
index 20b44a4..26b5fd2 100644
--- a/stackify/handler.py
+++ b/stackify/handler.py
@@ -1,10 +1,10 @@
+import copy
import logging
-import threading
-import os
+import atexit
try:
from logging.handlers import QueueHandler, QueueListener
-except: # pragma: no cover
+except Exception: # pragma: no cover
from stackify.handler_backport import QueueHandler, QueueListener
try:
@@ -12,11 +12,14 @@
except ImportError: # pragma: no cover
import queue
-from stackify import QUEUE_SIZE, API_URL, MAX_BATCH
-from stackify.log import LogMsg, LogMsgGroup
-from stackify.error import ErrorItem
-from stackify.http import HTTPClient
-from stackify.application import get_configuration
+from stackify.constants import API_REQUEST_INTERVAL_IN_SEC
+from stackify.constants import MAX_BATCH
+from stackify.constants import QUEUE_SIZE
+from stackify.timer import RepeatedTimer
+from stackify.transport import configure_transport
+
+
+internal_logger = logging.getLogger(__name__)
class StackifyHandler(QueueHandler):
@@ -25,10 +28,9 @@ class StackifyHandler(QueueHandler):
transmission to Stackify servers.
'''
- def __init__(self, queue_=None, listener=None, **kwargs):
+ def __init__(self, queue_=None, listener=None, ensure_at_exit=True, **kwargs):
if queue_ is None:
queue_ = queue.Queue(QUEUE_SIZE)
- logger = logging.getLogger(__name__)
super(StackifyHandler, self).__init__(queue_)
@@ -36,6 +38,11 @@ def __init__(self, queue_=None, listener=None, **kwargs):
listener = StackifyListener(queue_, **kwargs)
self.listener = listener
+ self.listener.start()
+
+ if ensure_at_exit:
+ internal_logger.debug('Registering atexit callback')
+ atexit.register(self.listener.stop)
def enqueue(self, record):
'''
@@ -44,12 +51,14 @@ def enqueue(self, record):
try:
self.queue.put_nowait(record)
except queue.Full:
- logger = logging.getLogger(__name__)
- logger.warn('StackifyHandler queue is full, '
- 'evicting oldest record')
+ internal_logger.warning('StackifyHandler queue is full, evicting oldest record')
self.queue.get_nowait()
self.queue.put_nowait(record)
+ def prepare(self, record):
+ record = copy.copy(record)
+ return record
+
class StackifyListener(QueueListener):
'''
@@ -59,43 +68,50 @@ class StackifyListener(QueueListener):
def __init__(self, queue_, max_batch=MAX_BATCH, config=None, **kwargs):
super(StackifyListener, self).__init__(queue_)
- if config is None:
- config = get_configuration(**kwargs)
-
self.max_batch = max_batch
self.messages = []
- self.http = HTTPClient(config)
+ self.transport = configure_transport(config, **kwargs)
+ self.timer = RepeatedTimer(API_REQUEST_INTERVAL_IN_SEC, self.send_group)
- def handle(self, record):
- if not self.http.identified:
- logger = logging.getLogger(__name__)
- logger.debug('Identifying application')
- self.http.identify_application()
+ self._started = False
- msg = LogMsg()
- msg.from_record(record)
- self.messages.append(msg)
+ def handle(self, record):
+ try:
+ self.messages.append(self.transport.create_message(record))
+ except Exception:
+ internal_logger.exception('Could not handle log message: {}'.format(hasattr(record, 'getMessage') and record.getMessage() or str(record)))
if len(self.messages) >= self.max_batch:
self.send_group()
def send_group(self):
- group = LogMsgGroup(self.messages)
+ if not self.messages:
+ return
+
+ group_message = self.transport.create_group_message(self.messages)
try:
- self.http.send_log_group(group)
- except:
- logger = logging.getLogger(__name__)
- logger.exception('Could not send %s log messages, discarding',
- len(self.messages))
+ self.transport.send(group_message)
+ except Exception:
+ internal_logger.exception('Could not send {} log messages, discarding'.format(len(self.messages)))
del self.messages[:]
+ def start(self):
+ internal_logger.debug('Starting up listener')
+
+ if not self._started:
+ super(StackifyListener, self).start()
+ self._started = True
+ self.timer.start()
+
def stop(self):
- logger = logging.getLogger(__name__)
- logger.debug('Shutting down listener')
- super(StackifyListener, self).stop()
+ internal_logger.debug('Shutting down listener')
+
+ if self._started:
+ super(StackifyListener, self).stop()
+ self.timer.stop()
+ self._started = False
# send any remaining messages
if self.messages:
- logger.debug('%s messages left on shutdown, uploading',
- len(self.messages))
+ internal_logger.debug('{} messages left on shutdown, uploading'.format(len(self.messages)))
self.send_group()
diff --git a/stackify/protos/__init__.py b/stackify/protos/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/stackify/protos/stackify_agent_pb2.py b/stackify/protos/stackify_agent_pb2.py
new file mode 100644
index 0000000..a8bbbc0
--- /dev/null
+++ b/stackify/protos/stackify_agent_pb2.py
@@ -0,0 +1,1127 @@
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: stackify-agent.proto
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf import descriptor_pb2
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+ name='stackify-agent.proto',
+ package='stackify',
+ syntax='proto3',
+ serialized_pb=_b('\n\x14stackify-agent.proto\x12\x08stackify\"\xba\x14\n\x08LogGroup\x12\x13\n\x0b\x65nvironment\x18\x01 \x01(\t\x12\x13\n\x0bserver_name\x18\x02 \x01(\t\x12\x18\n\x10\x61pplication_name\x18\x03 \x01(\t\x12\x1c\n\x14\x61pplication_location\x18\x04 \x01(\t\x12\x0e\n\x06logger\x18\x05 \x01(\t\x12\x10\n\x08platform\x18\x06 \x01(\t\x12$\n\x04logs\x18\x07 \x03(\x0b\x32\x16.stackify.LogGroup.Log\x12/\n\tcontainer\x18\x08 \x01(\x0b\x32\x1c.stackify.LogGroup.Container\x12\x31\n\nkubernetes\x18\t \x01(\x0b\x32\x1d.stackify.LogGroup.Kubernetes\x1ax\n\tContainer\x12\x10\n\x08image_id\x18\x01 \x01(\t\x12\x18\n\x10image_repository\x18\x02 \x01(\t\x12\x11\n\timage_tag\x18\x03 \x01(\t\x12\x14\n\x0c\x63ontainer_id\x18\x04 \x01(\t\x12\x16\n\x0e\x63ontainer_name\x18\x05 \x01(\t\x1aK\n\nKubernetes\x12\x10\n\x08pod_name\x18\x01 \x01(\t\x12\x15\n\rpod_namespace\x18\x02 \x01(\t\x12\x14\n\x0c\x63luster_name\x18\x03 \x01(\t\x1a\xd8\x10\n\x03Log\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\t\x12\x13\n\x0bthread_name\x18\x03 \x01(\t\x12\x13\n\x0b\x64\x61te_millis\x18\x04 \x01(\x03\x12\r\n\x05level\x18\x05 \x01(\t\x12\x16\n\x0etransaction_id\x18\x06 \x01(\t\x12\x15\n\rsource_method\x18\x07 \x01(\t\x12\x13\n\x0bsource_line\x18\x08 \x01(\x05\x12\n\n\x02id\x18\t \x01(\t\x12\x0c\n\x04tags\x18\n \x03(\t\x12+\n\x05\x65rror\x18\x0b \x01(\x0b\x32\x1c.stackify.LogGroup.Log.Error\x1a\xed\x0e\n\x05\x45rror\x12J\n\x12\x65nvironment_detail\x18\x01 \x01(\x0b\x32..stackify.LogGroup.Log.Error.EnvironmentDetail\x12\x13\n\x0b\x64\x61te_millis\x18\x02 \x01(\x03\x12:\n\nerror_item\x18\x03 \x01(\x0b\x32&.stackify.LogGroup.Log.Error.ErrorItem\x12I\n\x12web_request_detail\x18\x04 \x01(\x0b\x32-.stackify.LogGroup.Log.Error.WebRequestDetail\x12K\n\x10server_variables\x18\x05 \x03(\x0b\x32\x31.stackify.LogGroup.Log.Error.ServerVariablesEntry\x12\x15\n\rcustomer_name\x18\x06 \x01(\t\x12\x10\n\x08username\x18\x07 \x01(\t\x1a\x36\n\x14ServerVariablesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a\xaa\x01\n\x11\x45nvironmentDetail\x12\x13\n\x0b\x64\x65vice_name\x18\x01 \x01(\t\x12\x18\n\x10\x61pplication_name\x18\x02 \x01(\t\x12\x1c\n\x14\x61pplication_location\x18\x03 \x01(\t\x12#\n\x1b\x63onfigured_application_name\x18\x04 \x01(\t\x12#\n\x1b\x63onfigured_environment_name\x18\x05 \x01(\t\x1a\x9b\x03\n\tErrorItem\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x12\n\nerror_type\x18\x02 \x01(\t\x12\x17\n\x0f\x65rror_type_code\x18\x03 \x01(\t\x12>\n\x04\x64\x61ta\x18\x04 \x03(\x0b\x32\x30.stackify.LogGroup.Log.Error.ErrorItem.DataEntry\x12\x15\n\rsource_method\x18\x05 \x01(\t\x12\x45\n\nstacktrace\x18\x06 \x03(\x0b\x32\x31.stackify.LogGroup.Log.Error.ErrorItem.TraceFrame\x12;\n\x0binner_error\x18\x07 \x01(\x0b\x32&.stackify.LogGroup.Log.Error.ErrorItem\x1a+\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1aH\n\nTraceFrame\x12\x15\n\rcode_filename\x18\x01 \x01(\t\x12\x13\n\x0bline_number\x18\x02 \x01(\x05\x12\x0e\n\x06method\x18\x03 \x01(\t\x1a\x82\x07\n\x10WebRequestDetail\x12\x17\n\x0fuser_ip_address\x18\x01 \x01(\t\x12\x13\n\x0bhttp_method\x18\x02 \x01(\t\x12\x18\n\x10request_protocol\x18\x03 \x01(\t\x12\x13\n\x0brequest_url\x18\x04 \x01(\t\x12\x18\n\x10request_url_root\x18\x05 \x01(\t\x12\x14\n\x0creferral_url\x18\x06 \x01(\t\x12K\n\x07headers\x18\x07 \x03(\x0b\x32:.stackify.LogGroup.Log.Error.WebRequestDetail.HeadersEntry\x12K\n\x07\x63ookies\x18\x08 \x03(\x0b\x32:.stackify.LogGroup.Log.Error.WebRequestDetail.CookiesEntry\x12S\n\x0bquerystring\x18\t \x03(\x0b\x32>.stackify.LogGroup.Log.Error.WebRequestDetail.QuerystringEntry\x12N\n\tpost_data\x18\n \x03(\x0b\x32;.stackify.LogGroup.Log.Error.WebRequestDetail.PostDataEntry\x12T\n\x0csession_data\x18\x0b \x03(\x0b\x32>.stackify.LogGroup.Log.Error.WebRequestDetail.SessionDataEntry\x12\x15\n\rpost_data_raw\x18\x0c \x01(\t\x12\x12\n\nmvc_action\x18\r \x01(\t\x12\x16\n\x0emvc_controller\x18\x0e \x01(\t\x12\x10\n\x08mvc_area\x18\x0f \x01(\t\x1a.\n\x0cHeadersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a.\n\x0c\x43ookiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a\x32\n\x10QuerystringEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a/\n\rPostDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a\x32\n\x10SessionDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42.\n\x1d\x63om.stackify.api.common.protoB\rStackifyProtob\x06proto3')
+)
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+
+
+
+_LOGGROUP_CONTAINER = _descriptor.Descriptor(
+ name='Container',
+ full_name='stackify.LogGroup.Container',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='image_id', full_name='stackify.LogGroup.Container.image_id', index=0,
+ number=1, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='image_repository', full_name='stackify.LogGroup.Container.image_repository', index=1,
+ number=2, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='image_tag', full_name='stackify.LogGroup.Container.image_tag', index=2,
+ number=3, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='container_id', full_name='stackify.LogGroup.Container.container_id', index=3,
+ number=4, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='container_name', full_name='stackify.LogGroup.Container.container_name', index=4,
+ number=5, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=317,
+ serialized_end=437,
+)
+
+_LOGGROUP_KUBERNETES = _descriptor.Descriptor(
+ name='Kubernetes',
+ full_name='stackify.LogGroup.Kubernetes',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='pod_name', full_name='stackify.LogGroup.Kubernetes.pod_name', index=0,
+ number=1, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='pod_namespace', full_name='stackify.LogGroup.Kubernetes.pod_namespace', index=1,
+ number=2, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='cluster_name', full_name='stackify.LogGroup.Kubernetes.cluster_name', index=2,
+ number=3, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=439,
+ serialized_end=514,
+)
+
+_LOGGROUP_LOG_ERROR_SERVERVARIABLESENTRY = _descriptor.Descriptor(
+ name='ServerVariablesEntry',
+ full_name='stackify.LogGroup.Log.Error.ServerVariablesEntry',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='key', full_name='stackify.LogGroup.Log.Error.ServerVariablesEntry.key', index=0,
+ number=1, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='value', full_name='stackify.LogGroup.Log.Error.ServerVariablesEntry.value', index=1,
+ number=2, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=_descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')),
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1111,
+ serialized_end=1165,
+)
+
+_LOGGROUP_LOG_ERROR_ENVIRONMENTDETAIL = _descriptor.Descriptor(
+ name='EnvironmentDetail',
+ full_name='stackify.LogGroup.Log.Error.EnvironmentDetail',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='device_name', full_name='stackify.LogGroup.Log.Error.EnvironmentDetail.device_name', index=0,
+ number=1, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='application_name', full_name='stackify.LogGroup.Log.Error.EnvironmentDetail.application_name', index=1,
+ number=2, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='application_location', full_name='stackify.LogGroup.Log.Error.EnvironmentDetail.application_location', index=2,
+ number=3, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='configured_application_name', full_name='stackify.LogGroup.Log.Error.EnvironmentDetail.configured_application_name', index=3,
+ number=4, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='configured_environment_name', full_name='stackify.LogGroup.Log.Error.EnvironmentDetail.configured_environment_name', index=4,
+ number=5, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1168,
+ serialized_end=1338,
+)
+
+_LOGGROUP_LOG_ERROR_ERRORITEM_DATAENTRY = _descriptor.Descriptor(
+ name='DataEntry',
+ full_name='stackify.LogGroup.Log.Error.ErrorItem.DataEntry',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='key', full_name='stackify.LogGroup.Log.Error.ErrorItem.DataEntry.key', index=0,
+ number=1, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='value', full_name='stackify.LogGroup.Log.Error.ErrorItem.DataEntry.value', index=1,
+ number=2, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=_descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')),
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1635,
+ serialized_end=1678,
+)
+
+_LOGGROUP_LOG_ERROR_ERRORITEM_TRACEFRAME = _descriptor.Descriptor(
+ name='TraceFrame',
+ full_name='stackify.LogGroup.Log.Error.ErrorItem.TraceFrame',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='code_filename', full_name='stackify.LogGroup.Log.Error.ErrorItem.TraceFrame.code_filename', index=0,
+ number=1, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='line_number', full_name='stackify.LogGroup.Log.Error.ErrorItem.TraceFrame.line_number', index=1,
+ number=2, type=5, cpp_type=1, label=1,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='method', full_name='stackify.LogGroup.Log.Error.ErrorItem.TraceFrame.method', index=2,
+ number=3, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1680,
+ serialized_end=1752,
+)
+
+_LOGGROUP_LOG_ERROR_ERRORITEM = _descriptor.Descriptor(
+ name='ErrorItem',
+ full_name='stackify.LogGroup.Log.Error.ErrorItem',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='message', full_name='stackify.LogGroup.Log.Error.ErrorItem.message', index=0,
+ number=1, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='error_type', full_name='stackify.LogGroup.Log.Error.ErrorItem.error_type', index=1,
+ number=2, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='error_type_code', full_name='stackify.LogGroup.Log.Error.ErrorItem.error_type_code', index=2,
+ number=3, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='data', full_name='stackify.LogGroup.Log.Error.ErrorItem.data', index=3,
+ number=4, type=11, cpp_type=10, label=3,
+ has_default_value=False, default_value=[],
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='source_method', full_name='stackify.LogGroup.Log.Error.ErrorItem.source_method', index=4,
+ number=5, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='stacktrace', full_name='stackify.LogGroup.Log.Error.ErrorItem.stacktrace', index=5,
+ number=6, type=11, cpp_type=10, label=3,
+ has_default_value=False, default_value=[],
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='inner_error', full_name='stackify.LogGroup.Log.Error.ErrorItem.inner_error', index=6,
+ number=7, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[_LOGGROUP_LOG_ERROR_ERRORITEM_DATAENTRY, _LOGGROUP_LOG_ERROR_ERRORITEM_TRACEFRAME, ],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1341,
+ serialized_end=1752,
+)
+
+_LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_HEADERSENTRY = _descriptor.Descriptor(
+ name='HeadersEntry',
+ full_name='stackify.LogGroup.Log.Error.WebRequestDetail.HeadersEntry',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='key', full_name='stackify.LogGroup.Log.Error.WebRequestDetail.HeadersEntry.key', index=0,
+ number=1, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='value', full_name='stackify.LogGroup.Log.Error.WebRequestDetail.HeadersEntry.value', index=1,
+ number=2, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=_descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')),
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=2406,
+ serialized_end=2452,
+)
+
+_LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_COOKIESENTRY = _descriptor.Descriptor(
+ name='CookiesEntry',
+ full_name='stackify.LogGroup.Log.Error.WebRequestDetail.CookiesEntry',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='key', full_name='stackify.LogGroup.Log.Error.WebRequestDetail.CookiesEntry.key', index=0,
+ number=1, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='value', full_name='stackify.LogGroup.Log.Error.WebRequestDetail.CookiesEntry.value', index=1,
+ number=2, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=_descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')),
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=2454,
+ serialized_end=2500,
+)
+
+_LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_QUERYSTRINGENTRY = _descriptor.Descriptor(
+ name='QuerystringEntry',
+ full_name='stackify.LogGroup.Log.Error.WebRequestDetail.QuerystringEntry',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='key', full_name='stackify.LogGroup.Log.Error.WebRequestDetail.QuerystringEntry.key', index=0,
+ number=1, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='value', full_name='stackify.LogGroup.Log.Error.WebRequestDetail.QuerystringEntry.value', index=1,
+ number=2, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=_descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')),
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=2502,
+ serialized_end=2552,
+)
+
+_LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_POSTDATAENTRY = _descriptor.Descriptor(
+ name='PostDataEntry',
+ full_name='stackify.LogGroup.Log.Error.WebRequestDetail.PostDataEntry',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='key', full_name='stackify.LogGroup.Log.Error.WebRequestDetail.PostDataEntry.key', index=0,
+ number=1, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='value', full_name='stackify.LogGroup.Log.Error.WebRequestDetail.PostDataEntry.value', index=1,
+ number=2, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=_descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')),
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=2554,
+ serialized_end=2601,
+)
+
+_LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_SESSIONDATAENTRY = _descriptor.Descriptor(
+ name='SessionDataEntry',
+ full_name='stackify.LogGroup.Log.Error.WebRequestDetail.SessionDataEntry',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='key', full_name='stackify.LogGroup.Log.Error.WebRequestDetail.SessionDataEntry.key', index=0,
+ number=1, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='value', full_name='stackify.LogGroup.Log.Error.WebRequestDetail.SessionDataEntry.value', index=1,
+ number=2, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=_descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')),
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=2603,
+ serialized_end=2653,
+)
+
+_LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL = _descriptor.Descriptor(
+ name='WebRequestDetail',
+ full_name='stackify.LogGroup.Log.Error.WebRequestDetail',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='user_ip_address', full_name='stackify.LogGroup.Log.Error.WebRequestDetail.user_ip_address', index=0,
+ number=1, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='http_method', full_name='stackify.LogGroup.Log.Error.WebRequestDetail.http_method', index=1,
+ number=2, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='request_protocol', full_name='stackify.LogGroup.Log.Error.WebRequestDetail.request_protocol', index=2,
+ number=3, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='request_url', full_name='stackify.LogGroup.Log.Error.WebRequestDetail.request_url', index=3,
+ number=4, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='request_url_root', full_name='stackify.LogGroup.Log.Error.WebRequestDetail.request_url_root', index=4,
+ number=5, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='referral_url', full_name='stackify.LogGroup.Log.Error.WebRequestDetail.referral_url', index=5,
+ number=6, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='headers', full_name='stackify.LogGroup.Log.Error.WebRequestDetail.headers', index=6,
+ number=7, type=11, cpp_type=10, label=3,
+ has_default_value=False, default_value=[],
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='cookies', full_name='stackify.LogGroup.Log.Error.WebRequestDetail.cookies', index=7,
+ number=8, type=11, cpp_type=10, label=3,
+ has_default_value=False, default_value=[],
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='querystring', full_name='stackify.LogGroup.Log.Error.WebRequestDetail.querystring', index=8,
+ number=9, type=11, cpp_type=10, label=3,
+ has_default_value=False, default_value=[],
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='post_data', full_name='stackify.LogGroup.Log.Error.WebRequestDetail.post_data', index=9,
+ number=10, type=11, cpp_type=10, label=3,
+ has_default_value=False, default_value=[],
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='session_data', full_name='stackify.LogGroup.Log.Error.WebRequestDetail.session_data', index=10,
+ number=11, type=11, cpp_type=10, label=3,
+ has_default_value=False, default_value=[],
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='post_data_raw', full_name='stackify.LogGroup.Log.Error.WebRequestDetail.post_data_raw', index=11,
+ number=12, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='mvc_action', full_name='stackify.LogGroup.Log.Error.WebRequestDetail.mvc_action', index=12,
+ number=13, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='mvc_controller', full_name='stackify.LogGroup.Log.Error.WebRequestDetail.mvc_controller', index=13,
+ number=14, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='mvc_area', full_name='stackify.LogGroup.Log.Error.WebRequestDetail.mvc_area', index=14,
+ number=15, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[_LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_HEADERSENTRY, _LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_COOKIESENTRY, _LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_QUERYSTRINGENTRY, _LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_POSTDATAENTRY, _LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_SESSIONDATAENTRY, ],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1755,
+ serialized_end=2653,
+)
+
+_LOGGROUP_LOG_ERROR = _descriptor.Descriptor(
+ name='Error',
+ full_name='stackify.LogGroup.Log.Error',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='environment_detail', full_name='stackify.LogGroup.Log.Error.environment_detail', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='date_millis', full_name='stackify.LogGroup.Log.Error.date_millis', index=1,
+ number=2, type=3, cpp_type=2, label=1,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='error_item', full_name='stackify.LogGroup.Log.Error.error_item', index=2,
+ number=3, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='web_request_detail', full_name='stackify.LogGroup.Log.Error.web_request_detail', index=3,
+ number=4, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='server_variables', full_name='stackify.LogGroup.Log.Error.server_variables', index=4,
+ number=5, type=11, cpp_type=10, label=3,
+ has_default_value=False, default_value=[],
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='customer_name', full_name='stackify.LogGroup.Log.Error.customer_name', index=5,
+ number=6, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='username', full_name='stackify.LogGroup.Log.Error.username', index=6,
+ number=7, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[_LOGGROUP_LOG_ERROR_SERVERVARIABLESENTRY, _LOGGROUP_LOG_ERROR_ENVIRONMENTDETAIL, _LOGGROUP_LOG_ERROR_ERRORITEM, _LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL, ],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=752,
+ serialized_end=2653,
+)
+
+_LOGGROUP_LOG = _descriptor.Descriptor(
+ name='Log',
+ full_name='stackify.LogGroup.Log',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='message', full_name='stackify.LogGroup.Log.message', index=0,
+ number=1, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='data', full_name='stackify.LogGroup.Log.data', index=1,
+ number=2, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='thread_name', full_name='stackify.LogGroup.Log.thread_name', index=2,
+ number=3, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='date_millis', full_name='stackify.LogGroup.Log.date_millis', index=3,
+ number=4, type=3, cpp_type=2, label=1,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='level', full_name='stackify.LogGroup.Log.level', index=4,
+ number=5, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='transaction_id', full_name='stackify.LogGroup.Log.transaction_id', index=5,
+ number=6, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='source_method', full_name='stackify.LogGroup.Log.source_method', index=6,
+ number=7, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='source_line', full_name='stackify.LogGroup.Log.source_line', index=7,
+ number=8, type=5, cpp_type=1, label=1,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='id', full_name='stackify.LogGroup.Log.id', index=8,
+ number=9, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='tags', full_name='stackify.LogGroup.Log.tags', index=9,
+ number=10, type=9, cpp_type=9, label=3,
+ has_default_value=False, default_value=[],
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='error', full_name='stackify.LogGroup.Log.error', index=10,
+ number=11, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[_LOGGROUP_LOG_ERROR, ],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=517,
+ serialized_end=2653,
+)
+
+_LOGGROUP = _descriptor.Descriptor(
+ name='LogGroup',
+ full_name='stackify.LogGroup',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='environment', full_name='stackify.LogGroup.environment', index=0,
+ number=1, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='server_name', full_name='stackify.LogGroup.server_name', index=1,
+ number=2, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='application_name', full_name='stackify.LogGroup.application_name', index=2,
+ number=3, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='application_location', full_name='stackify.LogGroup.application_location', index=3,
+ number=4, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='logger', full_name='stackify.LogGroup.logger', index=4,
+ number=5, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='platform', full_name='stackify.LogGroup.platform', index=5,
+ number=6, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='logs', full_name='stackify.LogGroup.logs', index=6,
+ number=7, type=11, cpp_type=10, label=3,
+ has_default_value=False, default_value=[],
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='container', full_name='stackify.LogGroup.container', index=7,
+ number=8, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='kubernetes', full_name='stackify.LogGroup.kubernetes', index=8,
+ number=9, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[_LOGGROUP_CONTAINER, _LOGGROUP_KUBERNETES, _LOGGROUP_LOG, ],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=35,
+ serialized_end=2653,
+)
+
+_LOGGROUP_CONTAINER.containing_type = _LOGGROUP
+_LOGGROUP_KUBERNETES.containing_type = _LOGGROUP
+_LOGGROUP_LOG_ERROR_SERVERVARIABLESENTRY.containing_type = _LOGGROUP_LOG_ERROR
+_LOGGROUP_LOG_ERROR_ENVIRONMENTDETAIL.containing_type = _LOGGROUP_LOG_ERROR
+_LOGGROUP_LOG_ERROR_ERRORITEM_DATAENTRY.containing_type = _LOGGROUP_LOG_ERROR_ERRORITEM
+_LOGGROUP_LOG_ERROR_ERRORITEM_TRACEFRAME.containing_type = _LOGGROUP_LOG_ERROR_ERRORITEM
+_LOGGROUP_LOG_ERROR_ERRORITEM.fields_by_name['data'].message_type = _LOGGROUP_LOG_ERROR_ERRORITEM_DATAENTRY
+_LOGGROUP_LOG_ERROR_ERRORITEM.fields_by_name['stacktrace'].message_type = _LOGGROUP_LOG_ERROR_ERRORITEM_TRACEFRAME
+_LOGGROUP_LOG_ERROR_ERRORITEM.fields_by_name['inner_error'].message_type = _LOGGROUP_LOG_ERROR_ERRORITEM
+_LOGGROUP_LOG_ERROR_ERRORITEM.containing_type = _LOGGROUP_LOG_ERROR
+_LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_HEADERSENTRY.containing_type = _LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL
+_LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_COOKIESENTRY.containing_type = _LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL
+_LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_QUERYSTRINGENTRY.containing_type = _LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL
+_LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_POSTDATAENTRY.containing_type = _LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL
+_LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_SESSIONDATAENTRY.containing_type = _LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL
+_LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL.fields_by_name['headers'].message_type = _LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_HEADERSENTRY
+_LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL.fields_by_name['cookies'].message_type = _LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_COOKIESENTRY
+_LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL.fields_by_name['querystring'].message_type = _LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_QUERYSTRINGENTRY
+_LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL.fields_by_name['post_data'].message_type = _LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_POSTDATAENTRY
+_LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL.fields_by_name['session_data'].message_type = _LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_SESSIONDATAENTRY
+_LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL.containing_type = _LOGGROUP_LOG_ERROR
+_LOGGROUP_LOG_ERROR.fields_by_name['environment_detail'].message_type = _LOGGROUP_LOG_ERROR_ENVIRONMENTDETAIL
+_LOGGROUP_LOG_ERROR.fields_by_name['error_item'].message_type = _LOGGROUP_LOG_ERROR_ERRORITEM
+_LOGGROUP_LOG_ERROR.fields_by_name['web_request_detail'].message_type = _LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL
+_LOGGROUP_LOG_ERROR.fields_by_name['server_variables'].message_type = _LOGGROUP_LOG_ERROR_SERVERVARIABLESENTRY
+_LOGGROUP_LOG_ERROR.containing_type = _LOGGROUP_LOG
+_LOGGROUP_LOG.fields_by_name['error'].message_type = _LOGGROUP_LOG_ERROR
+_LOGGROUP_LOG.containing_type = _LOGGROUP
+_LOGGROUP.fields_by_name['logs'].message_type = _LOGGROUP_LOG
+_LOGGROUP.fields_by_name['container'].message_type = _LOGGROUP_CONTAINER
+_LOGGROUP.fields_by_name['kubernetes'].message_type = _LOGGROUP_KUBERNETES
+DESCRIPTOR.message_types_by_name['LogGroup'] = _LOGGROUP
+
+LogGroup = _reflection.GeneratedProtocolMessageType('LogGroup', (_message.Message,), dict(
+
+ Container = _reflection.GeneratedProtocolMessageType('Container', (_message.Message,), dict(
+ DESCRIPTOR = _LOGGROUP_CONTAINER,
+ __module__ = 'stackify_agent_pb2'
+ # @@protoc_insertion_point(class_scope:stackify.LogGroup.Container)
+ ))
+ ,
+
+ Kubernetes = _reflection.GeneratedProtocolMessageType('Kubernetes', (_message.Message,), dict(
+ DESCRIPTOR = _LOGGROUP_KUBERNETES,
+ __module__ = 'stackify_agent_pb2'
+ # @@protoc_insertion_point(class_scope:stackify.LogGroup.Kubernetes)
+ ))
+ ,
+
+ Log = _reflection.GeneratedProtocolMessageType('Log', (_message.Message,), dict(
+
+ Error = _reflection.GeneratedProtocolMessageType('Error', (_message.Message,), dict(
+
+ ServerVariablesEntry = _reflection.GeneratedProtocolMessageType('ServerVariablesEntry', (_message.Message,), dict(
+ DESCRIPTOR = _LOGGROUP_LOG_ERROR_SERVERVARIABLESENTRY,
+ __module__ = 'stackify_agent_pb2'
+ # @@protoc_insertion_point(class_scope:stackify.LogGroup.Log.Error.ServerVariablesEntry)
+ ))
+ ,
+
+ EnvironmentDetail = _reflection.GeneratedProtocolMessageType('EnvironmentDetail', (_message.Message,), dict(
+ DESCRIPTOR = _LOGGROUP_LOG_ERROR_ENVIRONMENTDETAIL,
+ __module__ = 'stackify_agent_pb2'
+ # @@protoc_insertion_point(class_scope:stackify.LogGroup.Log.Error.EnvironmentDetail)
+ ))
+ ,
+
+ ErrorItem = _reflection.GeneratedProtocolMessageType('ErrorItem', (_message.Message,), dict(
+
+ DataEntry = _reflection.GeneratedProtocolMessageType('DataEntry', (_message.Message,), dict(
+ DESCRIPTOR = _LOGGROUP_LOG_ERROR_ERRORITEM_DATAENTRY,
+ __module__ = 'stackify_agent_pb2'
+ # @@protoc_insertion_point(class_scope:stackify.LogGroup.Log.Error.ErrorItem.DataEntry)
+ ))
+ ,
+
+ TraceFrame = _reflection.GeneratedProtocolMessageType('TraceFrame', (_message.Message,), dict(
+ DESCRIPTOR = _LOGGROUP_LOG_ERROR_ERRORITEM_TRACEFRAME,
+ __module__ = 'stackify_agent_pb2'
+ # @@protoc_insertion_point(class_scope:stackify.LogGroup.Log.Error.ErrorItem.TraceFrame)
+ ))
+ ,
+ DESCRIPTOR = _LOGGROUP_LOG_ERROR_ERRORITEM,
+ __module__ = 'stackify_agent_pb2'
+ # @@protoc_insertion_point(class_scope:stackify.LogGroup.Log.Error.ErrorItem)
+ ))
+ ,
+
+ WebRequestDetail = _reflection.GeneratedProtocolMessageType('WebRequestDetail', (_message.Message,), dict(
+
+ HeadersEntry = _reflection.GeneratedProtocolMessageType('HeadersEntry', (_message.Message,), dict(
+ DESCRIPTOR = _LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_HEADERSENTRY,
+ __module__ = 'stackify_agent_pb2'
+ # @@protoc_insertion_point(class_scope:stackify.LogGroup.Log.Error.WebRequestDetail.HeadersEntry)
+ ))
+ ,
+
+ CookiesEntry = _reflection.GeneratedProtocolMessageType('CookiesEntry', (_message.Message,), dict(
+ DESCRIPTOR = _LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_COOKIESENTRY,
+ __module__ = 'stackify_agent_pb2'
+ # @@protoc_insertion_point(class_scope:stackify.LogGroup.Log.Error.WebRequestDetail.CookiesEntry)
+ ))
+ ,
+
+ QuerystringEntry = _reflection.GeneratedProtocolMessageType('QuerystringEntry', (_message.Message,), dict(
+ DESCRIPTOR = _LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_QUERYSTRINGENTRY,
+ __module__ = 'stackify_agent_pb2'
+ # @@protoc_insertion_point(class_scope:stackify.LogGroup.Log.Error.WebRequestDetail.QuerystringEntry)
+ ))
+ ,
+
+ PostDataEntry = _reflection.GeneratedProtocolMessageType('PostDataEntry', (_message.Message,), dict(
+ DESCRIPTOR = _LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_POSTDATAENTRY,
+ __module__ = 'stackify_agent_pb2'
+ # @@protoc_insertion_point(class_scope:stackify.LogGroup.Log.Error.WebRequestDetail.PostDataEntry)
+ ))
+ ,
+
+ SessionDataEntry = _reflection.GeneratedProtocolMessageType('SessionDataEntry', (_message.Message,), dict(
+ DESCRIPTOR = _LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_SESSIONDATAENTRY,
+ __module__ = 'stackify_agent_pb2'
+ # @@protoc_insertion_point(class_scope:stackify.LogGroup.Log.Error.WebRequestDetail.SessionDataEntry)
+ ))
+ ,
+ DESCRIPTOR = _LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL,
+ __module__ = 'stackify_agent_pb2'
+ # @@protoc_insertion_point(class_scope:stackify.LogGroup.Log.Error.WebRequestDetail)
+ ))
+ ,
+ DESCRIPTOR = _LOGGROUP_LOG_ERROR,
+ __module__ = 'stackify_agent_pb2'
+ # @@protoc_insertion_point(class_scope:stackify.LogGroup.Log.Error)
+ ))
+ ,
+ DESCRIPTOR = _LOGGROUP_LOG,
+ __module__ = 'stackify_agent_pb2'
+ # @@protoc_insertion_point(class_scope:stackify.LogGroup.Log)
+ ))
+ ,
+ DESCRIPTOR = _LOGGROUP,
+ __module__ = 'stackify_agent_pb2'
+ # @@protoc_insertion_point(class_scope:stackify.LogGroup)
+ ))
+_sym_db.RegisterMessage(LogGroup)
+_sym_db.RegisterMessage(LogGroup.Container)
+_sym_db.RegisterMessage(LogGroup.Kubernetes)
+_sym_db.RegisterMessage(LogGroup.Log)
+_sym_db.RegisterMessage(LogGroup.Log.Error)
+_sym_db.RegisterMessage(LogGroup.Log.Error.ServerVariablesEntry)
+_sym_db.RegisterMessage(LogGroup.Log.Error.EnvironmentDetail)
+_sym_db.RegisterMessage(LogGroup.Log.Error.ErrorItem)
+_sym_db.RegisterMessage(LogGroup.Log.Error.ErrorItem.DataEntry)
+_sym_db.RegisterMessage(LogGroup.Log.Error.ErrorItem.TraceFrame)
+_sym_db.RegisterMessage(LogGroup.Log.Error.WebRequestDetail)
+_sym_db.RegisterMessage(LogGroup.Log.Error.WebRequestDetail.HeadersEntry)
+_sym_db.RegisterMessage(LogGroup.Log.Error.WebRequestDetail.CookiesEntry)
+_sym_db.RegisterMessage(LogGroup.Log.Error.WebRequestDetail.QuerystringEntry)
+_sym_db.RegisterMessage(LogGroup.Log.Error.WebRequestDetail.PostDataEntry)
+_sym_db.RegisterMessage(LogGroup.Log.Error.WebRequestDetail.SessionDataEntry)
+
+
+DESCRIPTOR.has_options = True
+DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('\n\035com.stackify.api.common.protoB\rStackifyProto'))
+_LOGGROUP_LOG_ERROR_SERVERVARIABLESENTRY.has_options = True
+_LOGGROUP_LOG_ERROR_SERVERVARIABLESENTRY._options = _descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001'))
+_LOGGROUP_LOG_ERROR_ERRORITEM_DATAENTRY.has_options = True
+_LOGGROUP_LOG_ERROR_ERRORITEM_DATAENTRY._options = _descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001'))
+_LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_HEADERSENTRY.has_options = True
+_LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_HEADERSENTRY._options = _descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001'))
+_LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_COOKIESENTRY.has_options = True
+_LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_COOKIESENTRY._options = _descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001'))
+_LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_QUERYSTRINGENTRY.has_options = True
+_LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_QUERYSTRINGENTRY._options = _descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001'))
+_LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_POSTDATAENTRY.has_options = True
+_LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_POSTDATAENTRY._options = _descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001'))
+_LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_SESSIONDATAENTRY.has_options = True
+_LOGGROUP_LOG_ERROR_WEBREQUESTDETAIL_SESSIONDATAENTRY._options = _descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001'))
+# @@protoc_insertion_point(module_scope)
diff --git a/stackify/rum.py b/stackify/rum.py
new file mode 100644
index 0000000..10f2f76
--- /dev/null
+++ b/stackify/rum.py
@@ -0,0 +1,86 @@
+import json
+import base64
+from stackify import config
+
+apm_installed = False
+
+try:
+ from stackifyapm import insert_rum_script as insert_rum_script_from_apm
+ apm_installed = True
+except (ImportError):
+ pass
+
+
+def insert_rum_script():
+ apm_rum_script = insert_rum_script_apm()
+ 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 ''
+
+ transaction_id = get_transaction_id()
+ if not transaction_id:
+ return ''
+
+ reporting_url = get_reporting_url()
+ if not reporting_url:
+ return ''
+
+ application_name = config.application
+ if not application_name:
+ return ''
+
+ environment = config.environment
+ if not environment:
+ return ''
+
+ 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 ''
+
+ return ''.format(
+ json.dumps(settings),
+ rum_script_url,
+ rum_key
+ )
+
+
+def get_transaction_id():
+ return ''
+
+
+def get_reporting_url():
+ return ''
+
+
+def insert_rum_script_apm():
+ if not is_apm_installed():
+ return None
+
+ return insert_rum_script_from_apm()
+
+
+def is_apm_installed():
+ return apm_installed
diff --git a/stackify/timer.py b/stackify/timer.py
new file mode 100644
index 0000000..5dd6c13
--- /dev/null
+++ b/stackify/timer.py
@@ -0,0 +1,38 @@
+import time
+from threading import Event, Thread
+
+
+class RepeatedTimer(object):
+ '''
+ Repeater class that call the function every interval seconds.
+ '''
+
+ def __init__(self, interval, function, *args, **kwargs):
+ self.interval = interval
+ self.function = function
+ self.args = args
+ self.kwargs = kwargs
+ self.started = time.time()
+ self.event = Event()
+ self.thread = Thread(target=self._target)
+ self._started = False
+
+ def _target(self):
+ while not self.event.wait(self._time):
+ self.function(*self.args, **self.kwargs)
+
+ @property
+ def _time(self):
+ return self.interval - ((time.time() - self.started) % self.interval)
+
+ def start(self):
+ if not self._started:
+ self._started = True
+ self.thread.setDaemon(True)
+ self.thread.start()
+
+ def stop(self):
+ if self._started:
+ self._started = False
+ self.event.set()
+ self.thread.join()
diff --git a/stackify/transport/__init__.py b/stackify/transport/__init__.py
new file mode 100644
index 0000000..1b57e5c
--- /dev/null
+++ b/stackify/transport/__init__.py
@@ -0,0 +1,54 @@
+import logging
+
+from stackify.constants import TRANSPORT_TYPE_AGENT_HTTP
+from stackify.constants import TRANSPORT_TYPE_AGENT_SOCKET
+from stackify.constants import TRANSPORT_TYPE_DEFAULT
+from stackify.transport.agent import AgentSocketTransport
+from stackify.transport.agent import AgentHTTPTransport
+from stackify.transport.application import get_configuration
+from stackify.transport.application import EnvironmentDetail
+from stackify.transport.default import DefaultTransport
+
+
+internal_logger = logging.getLogger(__name__)
+
+
+class TransportTypes(object):
+ """
+ Transport Type class that will determine which transport to use
+ depending on users config.
+
+ Types:
+ * DEFAULT - HTTP transport that will directly send logs to the Platform
+ * AGENT_SOCKET - HTTP warapped Unix Socket Domain that will send logs to the StackifyAgent
+ * AGENT_HTTP - HTTP transport that will send logs to the Agent using HTTP requests
+ """
+
+ DEFAULT = TRANSPORT_TYPE_DEFAULT
+ AGENT_SOCKET = TRANSPORT_TYPE_AGENT_SOCKET
+ AGENT_HTTP = TRANSPORT_TYPE_AGENT_HTTP
+
+ @classmethod
+ def get_transport(self, api_config=None, env_details=None):
+ # determine which transport to use depening on users config
+ if api_config.transport == self.AGENT_SOCKET:
+ internal_logger.debug('Setting Agent Socket Transport.')
+ return AgentSocketTransport(api_config, env_details)
+
+ if api_config.transport == self.AGENT_HTTP:
+ internal_logger.debug('Setting Agent HTTP Transport.')
+ return AgentHTTPTransport(api_config, env_details)
+
+ internal_logger.debug('Setting Default Transport.')
+ api_config.transport = self.DEFAULT
+ return DefaultTransport(api_config, env_details)
+
+
+def configure_transport(config=None, **kwargs):
+ # return which transport to use depending on users input
+ api_config = config or get_configuration(**kwargs)
+ env_details = EnvironmentDetail(api_config)
+ return TransportTypes.get_transport(
+ api_config,
+ env_details,
+ )
diff --git a/stackify/transport/agent/__init__.py b/stackify/transport/agent/__init__.py
new file mode 100644
index 0000000..7ebc987
--- /dev/null
+++ b/stackify/transport/agent/__init__.py
@@ -0,0 +1,30 @@
+import logging
+
+from stackify.constants import AGENT_LOG_URL
+from stackify.transport.agent import agent_http
+from stackify.transport.agent import agent_socket
+from stackify.transport.base import AgentBaseTransport
+
+internal_logger = logging.getLogger(__name__)
+
+
+class AgentSocketTransport(AgentBaseTransport):
+ """
+ Agent Socket Transport handles sending of logs using Unix Socket Domain
+ """
+
+ def __init__(self, api_config, env_details):
+ super(AgentSocketTransport, self).__init__(api_config, env_details)
+ self.url = api_config.socket_url + AGENT_LOG_URL
+ self._transport = agent_socket.AgentSocket()
+
+
+class AgentHTTPTransport(AgentBaseTransport):
+ """
+ Agent HTTP Transport handles sending of logs using HTTP requests
+ """
+
+ def __init__(self, api_config, env_details):
+ super(AgentHTTPTransport, self).__init__(api_config, env_details)
+ self.url = api_config.http_endpoint + AGENT_LOG_URL
+ self._transport = agent_http.AgentHTTP()
diff --git a/stackify/transport/agent/agent_http.py b/stackify/transport/agent/agent_http.py
new file mode 100644
index 0000000..4b47d73
--- /dev/null
+++ b/stackify/transport/agent/agent_http.py
@@ -0,0 +1,26 @@
+import logging
+import requests
+import retrying
+
+internal_logger = logging.getLogger(__name__)
+
+
+class AgentHTTP(object):
+ """
+ AgentHTTP class that handles HTTP post requests
+ """
+
+ def _post(self, url, payload):
+ headers = {
+ 'Content-Type': 'application/x-protobuf',
+ }
+ try:
+ return requests.post(url, payload, headers=headers, verify=False)
+ except Exception as e:
+ internal_logger.debug('HTTP transport exception: {}.'.format(e))
+ raise
+
+ @retrying.retry(wait_exponential_multiplier=1000, stop_max_delay=32000)
+ def send(self, url, payload):
+ # send payload through socket domain using _post method
+ return self._post(url, payload)
diff --git a/stackify/transport/agent/agent_socket.py b/stackify/transport/agent/agent_socket.py
new file mode 100644
index 0000000..af0db72
--- /dev/null
+++ b/stackify/transport/agent/agent_socket.py
@@ -0,0 +1,45 @@
+import logging
+import os
+import retrying
+import requests_unixsocket
+
+
+internal_logger = logging.getLogger(__name__)
+
+
+class AgentSocket(object):
+ """
+ AgentSocket class that will post message through unix socket domain
+ """
+
+ SOCKET_LOG_FILE = 'http+unix://%2Fusr%2Flocal%2Fstackify%2Fstackify.sock'
+ SOCKET_SCHEME = 'http+unix://'
+
+ def __init__(self):
+ self._session = requests_unixsocket.Session()
+
+ def _post(self, url, payload):
+ # will use stackify default domain socket if url is not given
+ # or not using http+unix://
+ if not url.startswith(self.SOCKET_SCHEME):
+ url = os.path.join(self.SOCKET_LOG_FILE, url.lstrip('/'))
+
+ internal_logger.debug('Request URL: {}'.format(url))
+ internal_logger.debug('POST data: {}'.format(payload))
+
+ headers = {
+ 'Content-Type': 'application/x-protobuf',
+ }
+
+ try:
+ response = self._session.post(url, payload, headers=headers)
+ internal_logger.debug('Response status: {}'.format(response.status_code))
+ return response
+ except Exception as e:
+ internal_logger.debug('HTTP UNIX Socket domain exception: {}.'.format(e))
+ raise
+
+ @retrying.retry(wait_exponential_multiplier=1000, stop_max_delay=32000)
+ def send(self, url, payload):
+ # send payload through socket domain using _post method
+ return self._post(url, payload)
diff --git a/stackify/transport/agent/message.py b/stackify/transport/agent/message.py
new file mode 100644
index 0000000..bb418e2
--- /dev/null
+++ b/stackify/transport/agent/message.py
@@ -0,0 +1,125 @@
+import sys
+import traceback
+
+from stackify.constants import RECORD_VARS
+from stackify.protos import stackify_agent_pb2
+from stackify.utils import data_to_json
+
+
+class BaseMessage(object):
+ """
+ Base Class wrapper for protobuf classes
+ This will help to create protobuf object with ease
+ """
+ obj = None
+
+ def get_object(self):
+ # return protobuf object
+ return self.obj
+
+
+class EnvironmentDetail(BaseMessage):
+ """
+ Class wrapper for protobuf LogGroup.Log.Error.EnvironmentDetail class
+ """
+
+ def __init__(self, api_config, environment_details):
+ self.obj = env_details = stackify_agent_pb2.LogGroup.Log.Error.EnvironmentDetail()
+ env_details.application_name = api_config.application
+ env_details.configured_application_name = api_config.application
+ env_details.configured_environment_name = api_config.environment
+ env_details.device_name = environment_details.deviceName
+ env_details.application_location = environment_details.appLocation
+
+
+class TraceFrame(BaseMessage):
+ """
+ Class wrapper for protobuf LogGroup.Log.Error.ErrorItem.TraceFrame class
+ """
+
+ def __init__(self, filename, lineno, method):
+ self.obj = trace_frame = stackify_agent_pb2.LogGroup.Log.Error.ErrorItem.TraceFrame()
+ trace_frame.code_filename = filename
+ trace_frame.line_number = lineno
+ trace_frame.method = method
+
+
+class ErrorItem(BaseMessage):
+ """
+ Class wrapper for protobuf LogGroup.Log.Error.ErrorItem.TraceFrame class
+ """
+
+ def __init__(self, exc_info):
+ self.obj = error_item = stackify_agent_pb2.LogGroup.Log.Error.ErrorItem()
+
+ if not exc_info:
+ type_, value, tb = sys.exc_info()
+ else:
+ type_, value, tb = exc_info
+
+ stacks = traceback.extract_tb(tb)
+
+ error_item.message = str(value)
+ error_item.error_type = type_.__name__
+ error_item.source_method = stacks[-1][2]
+
+ for filename, lineno, method, text in reversed(stacks):
+ error_item.stacktrace.append(TraceFrame(filename, lineno, method).get_object())
+
+
+class Error(BaseMessage):
+ """
+ Class wrapper for protobuf LogGroup.Log.Error class
+ """
+
+ def __init__(self, record, api_config, env_details):
+ self.obj = error = stackify_agent_pb2.LogGroup.Log.Error()
+ error.date_millis = int(record.created * 1000)
+ error.error_item.MergeFrom(ErrorItem(record.exc_info).get_object())
+ error.environment_detail.MergeFrom(EnvironmentDetail(api_config, env_details).get_object())
+
+
+class Log(BaseMessage):
+ """
+ Class wrapper for protobuf LogGroup.Log class
+ """
+
+ def __init__(self, record, api_config, env_details):
+ self.obj = log = stackify_agent_pb2.LogGroup.Log()
+ log.message = record.getMessage()
+ log.thread_name = record.threadName or record.thread
+ log.date_millis = int(record.created * 1000)
+ log.level = record.levelname
+ log.source_method = record.funcName
+ log.source_line = record.lineno
+
+ if hasattr(record, 'log_id'):
+ log.id = record.log_id
+
+ if hasattr(record, 'trans_id'):
+ log.transaction_id = record.trans_id
+
+ data = {k: v for k, v in record.__dict__.items()
+ if k not in RECORD_VARS}
+
+ if data:
+ log.data = data_to_json(data)
+
+ if record.exc_info:
+ log.error.MergeFrom(Error(record, api_config, env_details).get_object())
+
+
+class LogGroup(BaseMessage):
+ """
+ Class wrapper for protobuf LogGroup class
+ """
+
+ def __init__(self, messages, api_config, env_details, logger=None):
+ self.obj = log_group = stackify_agent_pb2.LogGroup()
+ log_group.environment = api_config.environment
+ log_group.application_name = api_config.application
+ log_group.server_name = env_details.deviceName
+ log_group.application_location = env_details.appLocation
+ log_group.logger = logger or __name__
+ log_group.platform = 'python'
+ log_group.logs.extend(messages)
diff --git a/stackify/transport/application.py b/stackify/transport/application.py
new file mode 100644
index 0000000..bcbdd66
--- /dev/null
+++ b/stackify/transport/application.py
@@ -0,0 +1,116 @@
+import socket
+import os
+import logging
+
+from stackify.utils import arg_or_env
+from stackify.constants import API_URL
+from stackify.constants import DEFAULT_HTTP_ENDPOINT
+from stackify.constants import SOCKET_URL
+from stackify.constants import TRANSPORT_TYPE_AGENT_HTTP
+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):
+ """
+ EnvironmentDetail class that stores application environment
+ and user define details
+ """
+
+ def __init__(self, api_config):
+ self.deviceName = socket.gethostname()
+ self.appLocation = os.getcwd()
+ self.configuredAppName = api_config.application
+ self.configuredEnvironmentName = api_config.environment
+
+
+class ApiConfiguration:
+ """
+ ApiConfiguration class that stores application configurations
+ """
+
+ def __init__(
+ self,
+ api_key,
+ application,
+ environment,
+ api_url=API_URL,
+ 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
+ self.application = application
+ self.environment = environment
+ self.socket_url = socket_url
+ 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):
+ """
+ return application configuration depending on users input,
+ application environment and application config
+ """
+
+ transport = arg_or_env('transport', kwargs, TRANSPORT_TYPE_DEFAULT)
+
+ if transport in [TRANSPORT_TYPE_AGENT_SOCKET, TRANSPORT_TYPE_AGENT_HTTP]:
+ api_key = arg_or_env('api_key', kwargs, '')
+ else:
+ api_key = arg_or_env('api_key', kwargs)
+
+ return ApiConfiguration(
+ application=arg_or_env('application', kwargs),
+ environment=arg_or_env('environment', kwargs),
+ api_key=api_key,
+ api_url=arg_or_env('api_url', kwargs, API_URL),
+ 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/transport/base.py b/stackify/transport/base.py
new file mode 100644
index 0000000..15c897e
--- /dev/null
+++ b/stackify/transport/base.py
@@ -0,0 +1,40 @@
+from stackify.transport.agent.message import Log
+from stackify.transport.agent.message import LogGroup
+
+
+class BaseTransport(object):
+ """
+ Base Transport
+ """
+ def __init__(self, api_config, env_details):
+ self._api_config = api_config
+ self._env_details = env_details
+
+ def create_message(self, record):
+ raise NotImplementedError
+
+ def create_group_message(self, messages):
+ raise NotImplementedError
+
+ def send(self, group_message):
+ raise NotImplementedError
+
+
+class AgentBaseTransport(BaseTransport):
+ """
+ Base Transport for protobuf data
+ """
+ url = None
+ _transport = None
+
+ def __init__(self, api_config, env_details):
+ super(AgentBaseTransport, self).__init__(api_config, env_details)
+
+ def create_message(self, record):
+ return Log(record, self._api_config, self._env_details).get_object()
+
+ def create_group_message(self, messages):
+ return LogGroup(messages, self._api_config, self._env_details).get_object()
+
+ def send(self, group_message):
+ return self._transport.send(self.url, group_message.SerializeToString())
diff --git a/stackify/transport/default/__init__.py b/stackify/transport/default/__init__.py
new file mode 100644
index 0000000..e4711a9
--- /dev/null
+++ b/stackify/transport/default/__init__.py
@@ -0,0 +1,30 @@
+from stackify.constants import LOG_SAVE_URL
+from stackify.transport.base import BaseTransport
+from stackify.transport.default.http import HTTPClient
+from stackify.transport.default.log import LogMsg
+from stackify.transport.default.log import LogMsgGroup
+
+
+class DefaultTransport(BaseTransport):
+ """
+ Default Transport handles sending of logs directly to platform
+ """
+ _transport = None
+
+ def __init__(self, api_config, env_details):
+ super(DefaultTransport, self).__init__(api_config, env_details)
+ self._transport = HTTPClient(api_config, env_details)
+
+ def create_message(self, record):
+ msg = LogMsg()
+ msg.from_record(record)
+ return msg
+
+ def create_group_message(self, messages):
+ return LogMsgGroup(messages)
+
+ def send(self, group_message):
+ self._transport.send(
+ LOG_SAVE_URL,
+ group_message,
+ )
diff --git a/stackify/error.py b/stackify/transport/default/error.py
similarity index 70%
rename from stackify/error.py
rename to stackify/transport/default/error.py
index 7ce5c42..c2ef4c4 100644
--- a/stackify/error.py
+++ b/stackify/transport/default/error.py
@@ -1,8 +1,7 @@
import traceback
-import time
import sys
-from stackify.formats import JSONObject
+from stackify.transport.default.formats import JSONObject
class ErrorItem(JSONObject):
@@ -37,31 +36,11 @@ def __init__(self, filename, lineno, method):
self.Method = method
-class WebRequestDetail(JSONObject):
- def __init__(self):
- self.UserIPAddress = None
- self.HttpMethod = None
- self.RequestProtocol = None
- self.RequestUrl = None
- self.RequestUrlRoot = None
- self.ReferralUrl = None
- self.Headers = {}
- self.Cookies = {}
- self.QueryString = {}
- self.PostData = {}
- self.SessionData = {}
- self.PostDataRaw = None
- self.MVCAction = None
- self.MVCController = None
- self.MVCArea = None
-
-
class StackifyError(JSONObject):
def __init__(self):
self.EnvironmentDetail = None # environment detail object
self.OccurredEpochMillis = None
self.Error = None # ErrorItem object
- self.WebRequestDetail = None # WebRequestDetail object
self.CustomerName = None
self.UserName = None
diff --git a/stackify/formats.py b/stackify/transport/default/formats.py
similarity index 100%
rename from stackify/formats.py
rename to stackify/transport/default/formats.py
diff --git a/stackify/http.py b/stackify/transport/default/http.py
similarity index 62%
rename from stackify/http.py
rename to stackify/transport/default/http.py
index 82b8ca7..666deff 100644
--- a/stackify/http.py
+++ b/stackify/transport/default/http.py
@@ -5,12 +5,18 @@
try:
from cStringIO import StringIO
-except:
+except Exception:
try:
from StringIO import StringIO
- except:
+ except Exception:
pass # python 3, we use a new function in gzip
+from stackify.constants import IDENTIFY_URL
+from stackify.constants import READ_TIMEOUT
+
+
+internal_logger = logging.getLogger(__name__)
+
def gzip_compress(data):
if hasattr(gzip, 'compress'):
@@ -23,14 +29,10 @@ def gzip_compress(data):
return s.getvalue()
-from stackify.application import EnvironmentDetail
-from stackify import READ_TIMEOUT
-
-
class HTTPClient:
- def __init__(self, api_config):
+ def __init__(self, api_config, env_detail):
self.api_config = api_config
- self.environment_detail = EnvironmentDetail(api_config)
+ self.environment_detail = env_detail
self.app_name_id = None
self.app_env_id = None
self.device_id = None
@@ -40,8 +42,7 @@ def __init__(self, api_config):
def POST(self, url, json_object, use_gzip=False):
request_url = self.api_config.api_url + url
- logger = logging.getLogger(__name__)
- logger.debug('Request URL: %s', request_url)
+ internal_logger.debug('Request URL: {}'.format(request_url))
headers = {
'Content-Type': 'application/json',
@@ -51,7 +52,7 @@ def POST(self, url, json_object, use_gzip=False):
try:
payload_data = json_object.toJSON()
- logger.debug('POST data: %s', payload_data)
+ internal_logger.debug('POST data: {}'.format(payload_data))
if use_gzip:
headers['Content-Encoding'] = 'gzip'
@@ -61,19 +62,18 @@ def POST(self, url, json_object, use_gzip=False):
data=payload_data,
headers=headers,
timeout=READ_TIMEOUT)
- logger.debug('Response: %s', response.text)
+ internal_logger.debug('Response: {}'.format(response.text))
return response.json()
except requests.exceptions.RequestException:
- logger.exception('HTTP exception')
- raise
- except ValueError as e:
+ internal_logger.exception('HTTP exception')
+ except ValueError:
# could not read json response
- logger.exception('Cannot decode JSON response')
- raise
+ internal_logger.exception('Cannot decode JSON response')
- @retrying.retry(wait_exponential_multiplier=1000, stop_max_delay=10000)
+ @retrying.retry(wait_exponential_multiplier=1000, stop_max_delay=32000)
def identify_application(self):
- result = self.POST('/Metrics/IdentifyApp', self.environment_detail)
+ internal_logger.debug('Identifying application')
+ result = self.POST(IDENTIFY_URL, self.environment_detail)
self.app_name_id = result.get('AppNameID')
self.app_env_id = result.get('AppEnvID')
self.device_id = result.get('DeviceID')
@@ -81,12 +81,19 @@ def identify_application(self):
self.device_alias = result.get('DeviceAlias')
self.identified = True
- @retrying.retry(wait_exponential_multiplier=1000, stop_max_delay=10000)
- def send_log_group(self, group):
+ @retrying.retry(wait_exponential_multiplier=1000, stop_max_delay=32000)
+ def send_log_group(self, url, group):
+ internal_logger.debug('Sending logs by group')
+ group.AppName = self.environment_detail.configuredAppName
+ group.Env = self.environment_detail.configuredEnvironmentName
group.CDID = self.device_id
group.CDAppID = self.device_app_id
group.AppNameID = self.app_name_id
- group.ServerName = self.device_alias
- if not group.ServerName:
- group.ServerName = self.environment_detail.deviceName
- self.POST('/Log/Save', group, True)
+ group.ServerName = group.ServerName or self.environment_detail.deviceName
+ self.POST(url, group, True)
+
+ def send(self, url, group):
+ if not self.identified:
+ self.identify_application()
+
+ self.send_log_group(url, group)
diff --git a/stackify/log.py b/stackify/transport/default/log.py
similarity index 68%
rename from stackify/log.py
rename to stackify/transport/default/log.py
index c51f5f3..6601bc0 100644
--- a/stackify/log.py
+++ b/stackify/transport/default/log.py
@@ -1,22 +1,13 @@
-import json
-import logging
-
-from stackify.formats import JSONObject
-
-from stackify import MAX_BATCH, LOGGING_LEVELS
-from stackify.error import StackifyError
-
-
-# this is used to separate builtin keys from user-specified keys
-RECORD_VARS = set(logging.LogRecord('', '', '', '',
- '', '', '', '').__dict__.keys())
-
-# the "message" attribute is saved on the record object by a Formatter
-RECORD_VARS.add('message')
+from stackify.constants import RECORD_VARS
+from stackify.transport.default.formats import JSONObject
+from stackify.transport.default.error import StackifyError
+from stackify.utils import data_to_json
+from stackify.utils import extract_request
class LogMsg(JSONObject):
def __init__(self):
+ self.ID = None
self.Msg = None
self.data = None
self.Ex = None # a StackifyError object
@@ -28,10 +19,12 @@ def __init__(self):
self.SrcLine = None
def from_record(self, record):
+ self.ID = hasattr(record, 'log_id') and record.log_id or None
self.Msg = record.getMessage()
self.Th = record.threadName or record.thread
self.EpochMs = int(record.created * 1000)
self.Level = record.levelname
+ self.TransID = hasattr(record, 'trans_id') and record.trans_id or None
self.SrcMethod = record.funcName
self.SrcLine = record.lineno
@@ -39,8 +32,10 @@ def from_record(self, record):
data = {k: v for k, v in record.__dict__.items()
if k not in RECORD_VARS}
+ data = extract_request(data)
+
if data:
- self.data = json.dumps(data, default=lambda x: x.__dict__)
+ self.data = data_to_json(data)
if record.exc_info:
self.Ex = StackifyError()
diff --git a/stackify/utils.py b/stackify/utils.py
new file mode 100644
index 0000000..03d9c4a
--- /dev/null
+++ b/stackify/utils.py
@@ -0,0 +1,84 @@
+import os
+import json
+import logging
+import re
+from stackify import compat
+
+internal_logger = logging.getLogger(__name__)
+
+
+def arg_or_env(name, args, default=None, env_key=None):
+ env_name = env_key or 'STACKIFY_{0}'.format(name.upper())
+ try:
+ value = args.get(name)
+ if not value:
+ value = os.environ[env_name]
+ return value
+ except KeyError:
+ if default is not None:
+ return default
+ else:
+ raise NameError('You must specify the keyword argument {0} or environment variable {1}'.format(name, env_name))
+
+
+def data_to_json(data):
+ try:
+ return json.dumps(data, default=lambda x: get_default_object(x))
+ except Exception as e:
+ internal_logger.exception('Failed to serialize object to json: {} - Exception: {}'.format(str(data), str(e)))
+ return str(data) # String representation of the object
+
+
+def extract_request(data):
+ if 'request' in data and "WSGIRequest" in str(data['request']):
+ new_request = {}
+ obj = data['request']
+
+ if hasattr(obj, 'path'):
+ new_request['path'] = obj.path
+
+ if hasattr(obj, 'method'):
+ new_request['method'] = obj.method
+
+ if hasattr(obj, 'POST'):
+ new_request['form'] = obj.POST
+
+ if hasattr(obj, 'GET'):
+ new_request['query'] = obj.GET
+
+ if hasattr(obj, 'content_type'):
+ new_request['content_type'] = obj.content_type
+
+ data['request'] = new_request
+
+ return data
+
+
+def get_default_object(obj):
+ if object_is_iterable(obj):
+ return [item for item in obj]
+
+ return hasattr(obj, '__dict__') and obj.__dict__ or obj.__str__()
+
+
+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-execute.sh b/test-docker-execute.sh
new file mode 100755
index 0000000..f87161f
--- /dev/null
+++ b/test-docker-execute.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+set -e
+
+function runFlake8() {
+ echo '<--------------------------------------------->'
+ # run flake8 and exit on error
+ # it will check the code base against coding style (PEP8) and programming errors
+ echo "Running flake8..."
+ flake8 || { echo 'You have increased the number of flake8 errors'; exit 1; }
+}
+
+function runPyTest() {
+ echo '<--------------------------------------------->'
+ echo "Python Version $(python --version)"
+ echo 'Running pytest...'
+ 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
+
+runPyTest
diff --git a/test-docker.sh b/test-docker.sh
new file mode 100755
index 0000000..e4f4dea
--- /dev/null
+++ b/test-docker.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+
+set -e
+
+# remove caches
+find . | grep -E "(__pycache__|\.pyc|\.pyo$)" | xargs rm -rf
+
+VERSIONS=('2.7' '3.4' '3.5' '3.6' '3.7' '3.8')
+# VERSIONS=('2.7')
+
+for i in "${VERSIONS[@]}"
+do
+
+ if [[ "$(docker images -q stackify-python-api-test-${i}:latest 2> /dev/null)" != "" ]]; then
+ echo "Delete stackify-python-api-test-${i}..."
+ docker rm stackify-python-api-test-${i} &>/dev/null
+ docker rmi stackify-python-api-test-${i}:latest &>/dev/null
+ fi
+
+ echo "Building stackify-python-api-test-${i}..."
+ 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
+
+ echo "Delete stackify-python-api-test-${i}..."
+ docker rm stackify-python-api-test-${i} &>/dev/null
+ docker rmi stackify-python-api-test-${i}:latest &>/dev/null
+
+done
+
+echo "Done"
diff --git a/test.sh b/test.sh
new file mode 100755
index 0000000..e4d34fc
--- /dev/null
+++ b/test.sh
@@ -0,0 +1,54 @@
+#!/bin/bash
+set -e
+
+
+VERSIONS=('2.7' '3.4' '3.5' '3.6' '3.7' '3.8')
+
+
+function runFlake8() {
+ echo '<--------------------------------------------->'
+ # run flake8 and exit on error
+ # it will check the code base against coding style (PEP8) and programming errors
+ echo "Running flake8..."
+ flake8 || { echo 'You increased the number of flak8 errors'; exit 1; }
+}
+
+function runPyTest() {
+ echo '<--------------------------------------------->'
+ python_version=${1}
+ test_venv="venv_test_${python_version//.}"
+
+ echo "Creating virtualenv ${test_venv}..."
+ virtualenv -p python${python_version} ${test_venv}
+
+ echo "Activating virtualenv ${test_venv}..."
+
+ if [ -f ${test_venv}/bin/activate ]; then
+ source ${test_venv}/bin/activate
+ fi
+
+ if [ -f ${test_venv}/Scripts/activate ]; then
+ source ${test_venv}/Scripts/activate
+ fi
+
+ echo 'Installing dependencies...'
+ pip install -r requirements.txt
+
+ runFlake8
+
+ echo 'Running pytest...'
+ py.test
+
+ echo "Deactivating virtualenv..."
+ deactivate
+}
+
+echo 'Removing all existing virtualenv'
+rm -rf venv_test_* | true
+
+for i in "${VERSIONS[@]}"
+do
+ runPyTest ${i}
+done
+
+echo 'Done'
diff --git a/tests/bases.py b/tests/bases.py
index 7c4e4b0..d7298bd 100644
--- a/tests/bases.py
+++ b/tests/bases.py
@@ -1,6 +1,12 @@
+import collections
+import logging
import os
-import unittest
import retrying
+import unittest
+
+old_retry = retrying.retry
+_LoggingWatcher = collections.namedtuple("_LoggingWatcher", ["records", "output"])
+
class ClearEnvTest(unittest.TestCase):
'''
@@ -15,6 +21,10 @@ def setUp(self):
'STACKIFY_ENVIRONMENT',
'STACKIFY_API_KEY',
'STACKIFY_API_URL',
+ 'STACKIFY_TRANSPORT',
+ 'STACKIFY_TRANSPORT_HTTP_ENDPOINT',
+ 'RETRACE_RUM_SCRIPT_URL',
+ 'RETRACE_RUM_KEY'
]
self.saved = {}
for key in to_save:
@@ -28,3 +38,110 @@ def tearDown(self):
os.environ[key] = item
del self.saved
+
+def fake_retry_decorator(retries):
+ def fake_retry(*args, **kwargs):
+ kwargs['wait_exponential_max'] = 0 # no delay between retries
+ kwargs['stop_max_attempt_number'] = retries
+
+ def inner(func):
+ return old_retry(*args, **kwargs)(func)
+ return inner
+ return fake_retry
+
+
+class _BaseTestCaseContext(object):
+
+ def __init__(self, test_case):
+ self.test_case = test_case
+
+ def _raiseFailure(self, standardMsg):
+ msg = self.test_case._formatMessage(self.msg, standardMsg)
+ raise self.test_case.failureException(msg)
+
+
+class _CapturingHandler(logging.Handler):
+ """
+ A logging handler capturing all (raw and formatted) logging output.
+ """
+
+ def __init__(self):
+ logging.Handler.__init__(self)
+ self.watcher = _LoggingWatcher([], [])
+
+ def flush(self):
+ pass
+
+ def emit(self, record):
+ self.watcher.records.append(record)
+ msg = self.format(record)
+ self.watcher.output.append(msg)
+
+
+class _AssertLogsContext(_BaseTestCaseContext):
+ """A context manager used to implement TestCase.assertLogs()."""
+
+ LOGGING_FORMAT = "%(levelname)s:%(name)s:%(message)s"
+
+ def __init__(self, test_case, logger_name, level):
+ _BaseTestCaseContext.__init__(self, test_case)
+ self.logger_name = logger_name
+ if level:
+ self.level = logging._levelNames.get(level, level)
+ else:
+ self.level = logging.INFO
+ self.msg = None
+
+ def __enter__(self):
+ if isinstance(self.logger_name, logging.Logger):
+ logger = self.logger = self.logger_name
+ else:
+ logger = self.logger = logging.getLogger(self.logger_name)
+ formatter = logging.Formatter(self.LOGGING_FORMAT)
+ handler = _CapturingHandler()
+ handler.setFormatter(formatter)
+ self.watcher = handler.watcher
+ self.old_handlers = logger.handlers[:]
+ self.old_level = logger.level
+ self.old_propagate = logger.propagate
+ logger.handlers = [handler]
+ logger.setLevel(self.level)
+ logger.propagate = False
+ return handler.watcher
+
+ def __exit__(self, exc_type, exc_value, tb):
+ self.logger.handlers = self.old_handlers
+ self.logger.propagate = self.old_propagate
+ self.logger.setLevel(self.old_level)
+ if exc_type is not None:
+ # let unexpected exceptions pass through
+ return False
+ if len(self.watcher.records) == 0:
+ self._raiseFailure(
+ "no logs of level {} or higher triggered on {}"
+ .format(logging.getLevelName(self.level), self.logger.name))
+
+
+class LogTestCase(unittest.TestCase):
+
+ def assertLogs(self, logger=None, level=None):
+ """Fail unless a log message of level *level* or higher is emitted
+ on *logger_name* or its children. If omitted, *level* defaults to
+ INFO and *logger* defaults to the root logger.
+
+ This method must be used as a context manager, and will yield
+ a recording object with two attributes: `output` and `records`.
+ At the end of the context manager, the `output` attribute will
+ be a list of the matching formatted log messages and the
+ `records` attribute will be a list of the corresponding LogRecord
+ objects.
+
+ Example::
+
+ with self.assertLogs('foo', level='INFO') as cm:
+ logging.getLogger('foo').info('first message')
+ logging.getLogger('foo.bar').error('second message')
+ self.assertEqual(cm.output, ['INFO:foo:first message',
+ 'ERROR:foo.bar:second message'])
+ """
+ return _AssertLogsContext(self, logger, level)
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..0a9a699
--- /dev/null
+++ b/tests/rum/test_rum_apm.py
@@ -0,0 +1,77 @@
+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
+ assert rum_data is ''
diff --git a/tests/test_application.py b/tests/test_application.py
deleted file mode 100644
index 7e43248..0000000
--- a/tests/test_application.py
+++ /dev/null
@@ -1,95 +0,0 @@
-"""
-Test the stackify.application module
-"""
-
-import unittest
-from mock import patch
-import os
-from .bases import ClearEnvTest
-
-from stackify import API_URL
-from stackify.application import get_configuration
-
-
-class TestConfig(ClearEnvTest):
- '''
- Test automatic configuration for the ApiConfiguration
- '''
-
- def test_required_kwargs(self):
- '''API configuration requires appname, env and key'''
- env_map = {}
-
- with patch.dict('os.environ', env_map):
- with self.assertRaises(NameError):
- get_configuration()
- with self.assertRaises(NameError):
- get_configuration(application='1')
- with self.assertRaises(NameError):
- get_configuration(application='1', environment='2')
- with self.assertRaises(NameError):
- get_configuration(application='1', environment='2', api_url='3')
-
- get_configuration(application='1', environment='2', api_key='3')
-
- def test_environment_config(self):
- '''API configuration can load from env vars'''
- env_map = {
- 'STACKIFY_APPLICATION': 'test1_appname',
- 'STACKIFY_ENVIRONMENT': 'test1_environment',
- 'STACKIFY_API_KEY': 'test1_apikey',
- 'STACKIFY_API_URL': 'test1_apiurl',
- }
-
- with patch.dict('os.environ', env_map):
- config = get_configuration()
-
- self.assertEqual(config.application, 'test1_appname')
- self.assertEqual(config.environment, 'test1_environment')
- self.assertEqual(config.api_key, 'test1_apikey')
- self.assertEqual(config.api_url, 'test1_apiurl')
-
- def test_kwarg_mix(self):
- '''API configuration can load from a mix of env vars and kwargs'''
- env_map = {
- 'STACKIFY_APPLICATION': 'test2_appname',
- 'STACKIFY_ENVIRONMENT': 'test2_environment',
- }
-
- with patch.dict('os.environ', env_map):
- config = get_configuration(api_key='test2_apikey', api_url='test2_apiurl')
-
- self.assertEqual(config.application, 'test2_appname')
- self.assertEqual(config.environment, 'test2_environment')
- self.assertEqual(config.api_key, 'test2_apikey')
- self.assertEqual(config.api_url, 'test2_apiurl')
-
- def test_kwargs(self):
- '''API configuration can load from kwargs'''
- config = get_configuration(
- application = 'test3_appname',
- environment = 'test3_environment',
- api_key = 'test3_apikey',
- api_url = 'test3_apiurl')
-
- self.assertEqual(config.application, 'test3_appname')
- self.assertEqual(config.environment, 'test3_environment')
- self.assertEqual(config.api_key, 'test3_apikey')
- self.assertEqual(config.api_url, 'test3_apiurl')
-
- def test_api_url_default(self):
- '''API URL is set automatically'''
- config = get_configuration(
- application = 'test4_appname',
- environment = 'test4_environment',
- api_key = 'test4_apikey')
-
- self.assertEqual(config.application, 'test4_appname')
- self.assertEqual(config.environment, 'test4_environment')
- self.assertEqual(config.api_key, 'test4_apikey')
- self.assertEqual(config.api_url, API_URL)
-
-
-if __name__=='__main__':
- unittest.main()
-
diff --git a/tests/test_compat.py b/tests/test_compat.py
new file mode 100644
index 0000000..d2dcfbd
--- /dev/null
+++ b/tests/test_compat.py
@@ -0,0 +1,38 @@
+from unittest import TestCase
+
+from stackify.compat import b
+from stackify.compat import iterkeys
+from stackify.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_handler.py b/tests/test_handler.py
index 0ac201b..acb3487 100644
--- a/tests/test_handler.py
+++ b/tests/test_handler.py
@@ -11,7 +11,7 @@
import queue
from stackify.handler import StackifyHandler, StackifyListener
-from stackify.application import ApiConfiguration
+from stackify.transport.application import ApiConfiguration
import logging
@@ -41,29 +41,30 @@ class TestListener(unittest.TestCase):
def setUp(self):
self.config = ApiConfiguration(
- application = 'test_appname',
- environment = 'test_environment',
- api_key = 'test_apikey',
- api_url = 'test_apiurl')
+ application='test_appname',
+ environment='test_environment',
+ api_key='test_apikey',
+ api_url='test_apiurl',
+ )
# don't print warnings on http crashes, so mute stackify logger
logging.getLogger('stackify').propagate = False
- @patch('stackify.handler.LogMsg')
- @patch('stackify.handler.StackifyListener.send_group')
- @patch('stackify.handler.HTTPClient.POST')
- def test_not_identified(self, post, send_group, logmsg):
+ @patch('stackify.transport.default.DefaultTransport.create_message')
+ @patch('stackify.transport.default.http.HTTPClient.POST')
+ def test_not_identified(self, post, logmsg):
'''The HTTPClient identifies automatically if needed'''
listener = StackifyListener(queue_=Mock(), config=self.config)
listener.handle(Mock())
- self.assertTrue(listener.http.identified)
+ listener.send_group()
+ self.assertTrue(listener.transport._transport.identified)
- @patch('stackify.handler.LogMsg')
- @patch('stackify.handler.LogMsgGroup')
- @patch('stackify.handler.HTTPClient.POST')
+ @patch('stackify.transport.default.DefaultTransport.create_message')
+ @patch('stackify.transport.default.DefaultTransport.create_group_message')
+ @patch('stackify.transport.default.http.HTTPClient.POST')
def test_send_group_if_needed(self, post, logmsggroup, logmsg):
'''The listener sends groups of messages'''
listener = StackifyListener(queue_=Mock(), max_batch=3, config=self.config)
- listener.http.identified = True
+ listener.transport._transport.identified = True
listener.handle(1)
self.assertFalse(post.called)
@@ -77,12 +78,12 @@ def test_send_group_if_needed(self, post, logmsggroup, logmsg):
self.assertEqual(post.call_count, 1)
self.assertEqual(len(listener.messages), 1)
- @patch('stackify.handler.LogMsg')
+ @patch('stackify.transport.default.DefaultTransport.create_message')
@patch('stackify.handler.StackifyListener.send_group')
def test_clear_queue_shutdown(self, send_group, logmsg):
'''The listener sends the leftover messages on the queue when shutting down'''
listener = StackifyListener(queue_=Mock(), max_batch=3, config=self.config)
- listener.http.identified = True
+ listener.transport._transport.identified = True
listener._thread = Mock()
listener.handle(1)
@@ -91,13 +92,13 @@ def test_clear_queue_shutdown(self, send_group, logmsg):
listener.stop()
self.assertTrue(send_group.called)
- @patch('stackify.handler.LogMsg')
- @patch('stackify.handler.LogMsgGroup')
- @patch('stackify.handler.HTTPClient.send_log_group')
+ @patch('stackify.transport.default.DefaultTransport.create_message')
+ @patch('stackify.transport.default.DefaultTransport.create_group_message')
+ @patch('stackify.transport.default.http.HTTPClient.send_log_group')
def test_send_group_crash(self, send_log_group, logmsggroup, logmsg):
'''The listener drops messages after retrying'''
listener = StackifyListener(queue_=Mock(), max_batch=3, config=self.config)
- listener.http.identified = True
+ listener.transport._transport.identified = True
send_log_group.side_effect = Exception
@@ -109,7 +110,25 @@ def test_send_group_crash(self, send_log_group, logmsggroup, logmsg):
self.assertEqual(len(listener.messages), 1)
self.assertEqual(send_log_group.call_count, 1)
+ @patch('stackify.transport.default.DefaultTransport.create_message')
+ @patch('stackify.transport.default.DefaultTransport.create_group_message')
+ @patch('stackify.transport.default.http.HTTPClient.send_log_group')
+ def test_create_message_crash(self, send_log_group, logmsggroup, logmsg):
+ '''The listener drops messages after retrying'''
+ listener = StackifyListener(queue_=Mock(), max_batch=3, config=self.config)
+ listener.transport._transport.identified = True
+
+ logmsg.side_effect = Exception
+
+ listener.handle(1)
+ listener.handle(2)
+ listener.handle(3)
+ self.assertEqual(len(listener.messages), 0)
+ listener.handle(4)
+ self.assertEqual(len(listener.messages), 0) # messages not created
+ self.assertEqual(logmsg.call_count, 4) # we called the function 4 times
+ self.assertEqual(send_log_group.call_count, 0) # since we have exceptions
-if __name__=='__main__':
- unittest.main()
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_init.py b/tests/test_init.py
index 3a24a8a..7e69676 100644
--- a/tests/test_init.py
+++ b/tests/test_init.py
@@ -3,12 +3,9 @@
"""
import unittest
-from mock import patch, Mock
+from mock import patch
from .bases import ClearEnvTest
-import os
-import atexit
-
import stackify
import logging
@@ -20,11 +17,11 @@ class TestInit(ClearEnvTest):
def setUp(self):
super(TestInit, self).setUp()
- self.config = stackify.ApiConfiguration(
- application = 'test_appname',
- environment = 'test_environment',
- api_key = 'test_apikey',
- api_url = 'test_apiurl')
+ self.config = stackify.transport.application.ApiConfiguration(
+ application='test_appname',
+ environment='test_environment',
+ api_key='test_apikey',
+ api_url='test_apiurl')
self.loggers = []
def tearDown(self):
@@ -46,7 +43,7 @@ def test_logger_no_config(self):
logger = stackify.getLogger(auto_shutdown=False)
self.loggers.append(logger)
- config = logger.handlers[0].listener.http.api_config
+ config = logger.handlers[0].listener.transport._api_config
self.assertEqual(config.application, 'test2_appname')
self.assertEqual(config.environment, 'test2_environment')
@@ -58,7 +55,7 @@ def test_logger_api_config(self):
logger = stackify.getLogger(config=self.config, auto_shutdown=False)
self.loggers.append(logger)
- config = logger.handlers[0].listener.http.api_config
+ config = logger.handlers[0].listener.transport._api_config
self.assertEqual(config.application, 'test_appname')
self.assertEqual(config.environment, 'test_environment')
@@ -82,14 +79,14 @@ def test_get_logger_defaults(self):
self.loggers.append(logger)
handler = logger.handlers[0]
- config = handler.listener.http.api_config
+ config = handler.listener.transport._api_config
self.assertEqual(logger.name, 'tests.test_init')
- self.assertEqual(config.api_url, stackify.API_URL)
- self.assertEqual(handler.listener.max_batch, stackify.MAX_BATCH)
- self.assertEqual(handler.queue.maxsize, stackify.QUEUE_SIZE)
+ self.assertEqual(config.api_url, stackify.constants.API_URL)
+ self.assertEqual(handler.listener.max_batch, stackify.constants.MAX_BATCH)
+ self.assertEqual(handler.queue.maxsize, stackify.constants.QUEUE_SIZE)
# nose will goof with the following assert
- #self.assertEqual(logger.getEffectiveLevel(), logging.WARNING)
+ # self.assertEqual(logger.getEffectiveLevel(), logging.WARNING)
def test_get_logger_reuse(self):
'''Grabbing a logger twice results in the same logger'''
@@ -112,6 +109,5 @@ def test_get_handlers(self):
self.assertEqual(logger.handlers, stackify.getHandlers(logger))
-if __name__=='__main__':
+if __name__ == '__main__':
unittest.main()
-
diff --git a/tests/test_rum.py b/tests/test_rum.py
new file mode 100644
index 0000000..b1e77fb
--- /dev/null
+++ b/tests/test_rum.py
@@ -0,0 +1,252 @@
+try:
+ from unittest import mock
+except Exception:
+ import mock # noqa F401
+
+import stackify
+import os
+import stackify.rum
+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 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
+
+ @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, func_apm):
+ func.return_value = '123'
+ func_reporting_url.return_value = 'test reporting url'
+ func_apm.return_value = 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()
+
+ assert result == ''.format(json.dumps(rum_settings))
+
+ @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',
+ environment='env'
+ )
+
+ result = stackify.rum.insert_rum_script()
+ self.reset_common_config()
+
+ assert not result
+ assert result is ''
+
+ @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',
+ environment='env'
+ )
+
+ result = stackify.rum.insert_rum_script()
+ assert not result
+ assert result is ''
+
+ self.reset_common_config()
+
+ @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
+ assert result is ''
+
+ self.reset_common_config()
+
+ @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, func_apm):
+ func.return_value = '123'
+ func_apm.return_value = False
+ func_reporting_url.return_value = 'test reporting url'
+ 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()
+ assert result == ''.format(json.dumps(rum_settings))
+
+ @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',
+ environment='env2'
+ )
+
+ result = stackify.rum.insert_rum_script()
+ 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):
+ func_apm.return_value = False
+ self.create_config(
+ application=None,
+ environment=None,
+ rum_key=None
+ )
+
+ result = stackify.rum.insert_rum_script()
+ 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
+ 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_timer.py b/tests/test_timer.py
new file mode 100644
index 0000000..09d8f9e
--- /dev/null
+++ b/tests/test_timer.py
@@ -0,0 +1,32 @@
+import time
+from unittest import TestCase
+try:
+ from unittest import mock
+except Exception:
+ import mock
+
+from stackify.timer import RepeatedTimer
+
+
+class TimerTest(TestCase):
+ def setUp(self):
+ self.function_mock = mock.Mock()
+ self.timer = RepeatedTimer(0.1, self.function_mock)
+ self.timer.start()
+
+ def shutDown(self):
+ self.timer.stop()
+
+ def test_start(self):
+ assert self.timer._started
+
+ def test_stop(self):
+ self.timer.stop()
+
+ assert not self.timer._started
+
+ def test_timer(self):
+ time.sleep(0.3)
+
+ assert self.function_mock.called
+ assert self.function_mock.call_count >= 2
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 0000000..3357a27
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,182 @@
+"""
+Test the stackify.__init__ setup functions
+"""
+
+import unittest
+from mock import patch
+from .bases import ClearEnvTest
+
+import stackify
+import logging
+
+from stackify.utils import ConfigError
+from stackify.utils import RegexValidator
+
+
+class TestInit(ClearEnvTest):
+ '''
+ Test the logger init functionality
+ '''
+
+ def setUp(self):
+ super(TestInit, self).setUp()
+ self.config = stackify.transport.application.ApiConfiguration(
+ application='test_appname',
+ environment='test_environment',
+ api_key='test_apikey',
+ api_url='test_apiurl')
+ self.loggers = []
+
+ def tearDown(self):
+ super(TestInit, self).tearDown()
+ global_loggers = logging.Logger.manager.loggerDict
+ for logger in self.loggers:
+ del global_loggers[logger.name]
+
+ def test_utils_data_to_json_string(self):
+ dummy = 'test'
+ result = stackify.utils.data_to_json(dummy)
+ self.assertEqual('"test"', result)
+
+ def test_utils_data_to_json_unserializable(self):
+ dummy = Dummy()
+ result = stackify.utils.data_to_json(dummy)
+ substring = '