Source code for pyams_apm.tween

#
# Copyright (c) 2008-2018 Thierry Florac <tflorac AT ulthar.net>
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#

"""PyAMS_apm.tween module

This module provides a custom tween which is used to integrate the Pyramid application
with APM, sending requests frames to the APM server.
"""

import sys
import pkg_resources

import elasticapm
from elasticapm.utils import compat, get_url_dict
from pyramid.compat import reraise
from pyramid.settings import asbool

__docformat__ = 'restructuredtext'


[docs]def list_from_setting(config, setting): """Split configuration setting""" value = config.get(setting) if not value: return None return value.split()
[docs]def get_data_from_request(request): """Extract main APM data from request properties""" data = { "headers": dict(**request.headers), "method": request.method, "socket": { "remote_address": request.remote_addr, "encrypted": request.scheme == 'https' }, "cookies": dict(**request.cookies), "url": get_url_dict(request.url) } # remove Cookie header since the same data is in request["cookies"] as well data["headers"].pop("Cookie", None) return data
[docs]def get_data_from_response(response): """Extract APM data from response properties""" data = {"status_code": response.status_int} if response.headers: data["headers"] = { key: ";".join(response.headers.getall(key)) for key in compat.iterkeys(response.headers) } return data
[docs]class elastic_apm_tween_factory: # pylint: disable=invalid-name """Elasticsearch APM tween factory""" def __init__(self, handler, registry): self.handler = handler self.registry = registry config = registry.settings service_version = config.get("elasticapm.service_version") if service_version: try: service_version = pkg_resources.get_distribution(service_version).version except pkg_resources.DistributionNotFound: pass self.client = elasticapm.Client( server_url=config.get("elasticapm.server_url"), server_timeout=config.get("elasticapm.server_timeout"), name=config.get("elasticapm.name"), framework_name="Pyramid", framework_version=pkg_resources.get_distribution("pyramid").version, service_name=config.get("elasticapm.service_name"), service_version=service_version, secret_token=config.get("elasticapm.secret_token"), include_paths=list_from_setting(config, "elasticapm.include_paths"), exclude_paths=list_from_setting(config, "elasticapm.exclude_paths"), debug=asbool(config.get('elasticapm.debug')) ) def __call__(self, request): self.client.begin_transaction('request') try: response = self.handler(request) transaction_result = response.status[0] + "xx" elasticapm.set_context(lambda: get_data_from_response(response), "response") return response except Exception: # pylint: disable=broad-except transaction_result = '5xx' self.client.capture_exception( context={ "request": get_data_from_request(request) }, handled=False, # indicate that this exception bubbled all the way up to the user ) reraise(*sys.exc_info()) finally: try: view_name = request.view_name if not view_name: view_name = request.traversed[-1] if request.traversed else '/' except AttributeError: view_name = '' transaction_name = request.matched_route.pattern if request.matched_route else view_name # prepend request method transaction_name = " ".join((request.method, transaction_name)) \ if transaction_name else "" elasticapm.set_context(lambda: get_data_from_request(request), "request") self.client.end_transaction(transaction_name, transaction_result)