Skip to content

Commit 9461255

Browse files
author
Andrew Slotin
authored
New Pyramid Instrumentation (#233)
* Attempt to determine the serice name even if the env variable is not set * Add Pyramid tween to instrument handlers * Add an example Pyramid app and instrumentation tests
1 parent baaa362 commit 9461255

File tree

7 files changed

+351
-9
lines changed

7 files changed

+351
-9
lines changed

instana/instrumentation/pyramid/__init__.py

Whitespace-only changes.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from __future__ import absolute_import
2+
3+
from pyramid.httpexceptions import HTTPException
4+
5+
import opentracing as ot
6+
import opentracing.ext.tags as ext
7+
8+
from ...log import logger
9+
from ...singletons import tracer, agent
10+
from ...util import strip_secrets
11+
12+
class InstanaTweenFactory(object):
13+
"""A factory that provides Instana instrumentation tween for Pyramid apps"""
14+
15+
def __init__(self, handler, registry):
16+
self.handler = handler
17+
18+
def __call__(self, request):
19+
ctx = tracer.extract(ot.Format.HTTP_HEADERS, request.headers)
20+
scope = tracer.start_active_span('http', child_of=ctx)
21+
22+
scope.span.set_tag(ext.SPAN_KIND, ext.SPAN_KIND_RPC_SERVER)
23+
scope.span.set_tag("http.host", request.host)
24+
scope.span.set_tag(ext.HTTP_METHOD, request.method)
25+
scope.span.set_tag(ext.HTTP_URL, request.path)
26+
27+
if request.matched_route is not None:
28+
scope.span.set_tag("http.path_tpl", request.matched_route.pattern)
29+
30+
if hasattr(agent, 'extra_headers') and agent.extra_headers is not None:
31+
for custom_header in agent.extra_headers:
32+
# Headers are available in this format: HTTP_X_CAPTURE_THIS
33+
h = ('HTTP_' + custom_header.upper()).replace('-', '_')
34+
if h in request.headers:
35+
scope.span.set_tag("http.%s" % custom_header, request.headers[h])
36+
37+
if len(request.query_string):
38+
scrubbed_params = strip_secrets(request.query_string, agent.secrets_matcher, agent.secrets_list)
39+
scope.span.set_tag("http.params", scrubbed_params)
40+
41+
response = None
42+
try:
43+
response = self.handler(request)
44+
45+
tracer.inject(scope.span.context, ot.Format.HTTP_HEADERS, response.headers)
46+
response.headers['Server-Timing'] = "intid;desc=%s" % scope.span.context.trace_id
47+
except HTTPException as e:
48+
response = e
49+
raise
50+
except BaseException as e:
51+
scope.span.set_tag("http.status", 500)
52+
53+
# we need to explicitly populate the `message` tag with an error here
54+
# so that it's picked up from an SDK span
55+
scope.span.set_tag("message", str(e))
56+
scope.span.log_exception(e)
57+
58+
logger.debug("Pyramid Instana tween", exc_info=True)
59+
finally:
60+
if response:
61+
scope.span.set_tag("http.status", response.status_int)
62+
63+
if 500 <= response.status_int <= 511:
64+
if response.exception is not None:
65+
message = str(response.exception)
66+
scope.span.log_exception(response.exception)
67+
else:
68+
message = response.status
69+
70+
scope.span.set_tag("message", message)
71+
scope.span.assure_errored()
72+
73+
scope.close()
74+
75+
return response
76+
77+
def includeme(config):
78+
logger.debug("Instrumenting pyramid")
79+
config.add_tween(__name__ + '.InstanaTweenFactory')

instana/options.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import logging
33
import os
44

5+
from .util import determine_service_name
56

67
class StandardOptions(object):
78
""" Configurable option bits for this package """
@@ -20,7 +21,7 @@ def __init__(self, **kwds):
2021
self.log_level = logging.DEBUG
2122
self.debug = True
2223

23-
self.service_name = os.environ.get("INSTANA_SERVICE_NAME", None)
24+
self.service_name = determine_service_name()
2425
self.agent_host = os.environ.get("INSTANA_AGENT_HOST", self.AGENT_DEFAULT_HOST)
2526
self.agent_port = os.environ.get("INSTANA_AGENT_PORT", self.AGENT_DEFAULT_PORT)
2627

setup.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def check_setuptools():
7171
'gevent>=1.4.0'
7272
'mock>=2.0.0',
7373
'nose>=1.0',
74+
'pyramid>=1.2',
7475
'urllib3[secure]>=1.15'
7576
],
7677
'test-cassandra': [
@@ -96,6 +97,7 @@ def check_setuptools():
9697
'pytest>=3.0.1',
9798
'psycopg2>=2.7.1',
9899
'pymongo>=3.7.0',
100+
'pyramid>=1.2',
99101
'redis>3.0.0',
100102
'requests>=2.17.1',
101103
'sqlalchemy>=1.1.15',
@@ -112,6 +114,7 @@ def check_setuptools():
112114
'Development Status :: 5 - Production/Stable',
113115
'Framework :: Django',
114116
'Framework :: Flask',
117+
'Framework :: Pyramid',
115118
'Intended Audience :: Developers',
116119
'Intended Audience :: Information Technology',
117120
'Intended Audience :: Science/Research',

tests/__init__.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,22 @@
1313

1414
if 'CASSANDRA_TEST' not in os.environ:
1515
from .apps.flaskalino import flask_server
16+
from .apps.app_pyramid import pyramid_server
1617

17-
# Background Flask application
18-
#
19-
# Spawn our background Flask app that the tests will throw
18+
# Background applications
19+
servers = {
20+
'Flask': flask_server,
21+
'Pyramid': pyramid_server,
22+
}
23+
24+
# Spawn background apps that the tests will throw
2025
# requests at.
21-
flask = threading.Thread(target=flask_server.serve_forever)
22-
flask.daemon = True
23-
flask.name = "Background Flask app"
24-
print("Starting background Flask app...")
25-
flask.start()
26+
for (name, server) in servers.items():
27+
p = threading.Thread(target=server.serve_forever)
28+
p.daemon = True
29+
p.name = "Background %s app" % name
30+
print("Starting background %s app..." % name)
31+
p.start()
2632

2733
if 'GEVENT_TEST' not in os.environ and 'CASSANDRA_TEST' not in os.environ:
2834

tests/apps/app_pyramid.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from wsgiref.simple_server import make_server
2+
from pyramid.config import Configurator
3+
import logging
4+
5+
from pyramid.response import Response
6+
import pyramid.httpexceptions as exc
7+
8+
from ..helpers import testenv
9+
10+
logging.basicConfig(level=logging.INFO)
11+
logger = logging.getLogger(__name__)
12+
13+
testenv["pyramid_port"] = 10815
14+
testenv["pyramid_server"] = ("http://127.0.0.1:" + str(testenv["pyramid_port"]))
15+
16+
def hello_world(request):
17+
return Response('Ok')
18+
19+
def please_fail(request):
20+
raise exc.HTTPInternalServerError("internal error")
21+
22+
def tableflip(request):
23+
raise BaseException("fake exception")
24+
25+
app = None
26+
with Configurator() as config:
27+
config.add_tween('instana.instrumentation.pyramid.tweens.InstanaTweenFactory')
28+
config.add_route('hello', '/')
29+
config.add_view(hello_world, route_name='hello')
30+
config.add_route('fail', '/500')
31+
config.add_view(please_fail, route_name='fail')
32+
config.add_route('crash', '/exception')
33+
config.add_view(tableflip, route_name='crash')
34+
app = config.make_wsgi_app()
35+
36+
pyramid_server = make_server('127.0.0.1', testenv["pyramid_port"], app)
37+

0 commit comments

Comments
 (0)