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 = '