src/pyams_apm/tween.py
changeset 0 1cc388b5cf69
child 2 e78763bdfb3f
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_apm/tween.py	Tue Sep 18 17:38:43 2018 +0200
@@ -0,0 +1,116 @@
+#
+# 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.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import pkg_resources
+from pkg_resources import DistributionNotFound
+
+import sys
+
+# import interfaces
+
+# import packages
+import elasticapm
+from elasticapm.utils import compat, get_url_dict
+from pyramid.settings import asbool
+from pyramid.compat import reraise
+
+
+def list_from_setting(config, setting):
+    value = config.get(setting)
+    if not value:
+        return None
+    return value.split()
+
+
+def get_data_from_request(request):
+    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
+
+
+def get_data_from_response(response):
+    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
+
+
+class elastic_apm_tween_factory(object):
+    """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 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:
+            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
+            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)