Skip to content

Commit 7d23a2e

Browse files
authored
Flask: Improved and expanded instrumentation (#106)
* New Flask instrumentation strategy * Flask rendering instrumentation & tests * General exception capture * Remove flask entry point; Update docs * Better live hooks; Render support and tests * Store context in flask.g; Remove debug * Scrub screts from params * Break out Flask to with and without blinker support * Do not use id_to_header * Set proper response headers * Render is a registered span * Fixed exception logging/reporting * Protect against potential None types * Update Flask tests to follow mainline updates * Fix path retrieval for requests package * Assure render spans are recorded as local
1 parent 295c348 commit 7d23a2e

16 files changed

+829
-71
lines changed

instana/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ def boot_agent():
6060
from .instrumentation.aiohttp import client
6161
from .instrumentation.aiohttp import server
6262
from .instrumentation import asynqp
63+
from .instrumentation import flask
6364
from .instrumentation.tornado import client
6465
from .instrumentation.tornado import server
6566
from .instrumentation import logging

instana/flaskana.py

-22
This file was deleted.
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from __future__ import absolute_import
2+
3+
try:
4+
import flask
5+
from flask.signals import signals_available
6+
7+
# `signals_available` indicates whether the Flask process is running with or without blinker support:
8+
# https://pypi.org/project/blinker/
9+
#
10+
# Blinker support is preferred but we do the best we can when it's not available.
11+
#
12+
13+
if signals_available is True:
14+
import instana.instrumentation.flask.with_blinker
15+
else:
16+
import instana.instrumentation.flask.vanilla
17+
except ImportError:
18+
pass
+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
from __future__ import absolute_import
2+
3+
import opentracing
4+
import opentracing.ext.tags as ext
5+
import wrapt
6+
7+
from ...log import logger
8+
from ...singletons import agent, tracer
9+
from ...util import strip_secrets
10+
11+
import flask
12+
13+
14+
def before_request_with_instana(*argv, **kwargs):
15+
try:
16+
env = flask.request.environ
17+
ctx = None
18+
19+
if 'HTTP_X_INSTANA_T' in env and 'HTTP_X_INSTANA_S' in env:
20+
ctx = tracer.extract(opentracing.Format.HTTP_HEADERS, env)
21+
22+
flask.g.scope = tracer.start_active_span('wsgi', child_of=ctx)
23+
span = flask.g.scope.span
24+
25+
if agent.extra_headers is not None:
26+
for custom_header in agent.extra_headers:
27+
# Headers are available in this format: HTTP_X_CAPTURE_THIS
28+
header = ('HTTP_' + custom_header.upper()).replace('-', '_')
29+
if header in env:
30+
span.set_tag("http.%s" % custom_header, env[header])
31+
32+
span.set_tag(ext.HTTP_METHOD, flask.request.method)
33+
if 'PATH_INFO' in env:
34+
span.set_tag(ext.HTTP_URL, env['PATH_INFO'])
35+
if 'QUERY_STRING' in env and len(env['QUERY_STRING']):
36+
scrubbed_params = strip_secrets(env['QUERY_STRING'], agent.secrets_matcher, agent.secrets_list)
37+
span.set_tag("http.params", scrubbed_params)
38+
if 'HTTP_HOST' in env:
39+
span.set_tag("http.host", env['HTTP_HOST'])
40+
except:
41+
logger.debug("Flask before_request", exc_info=True)
42+
finally:
43+
return None
44+
45+
46+
def after_request_with_instana(response):
47+
try:
48+
scope = None
49+
50+
# If we're not tracing, just return
51+
if not hasattr(flask.g, 'scope'):
52+
return response
53+
54+
scope = flask.g.scope
55+
span = scope.span
56+
57+
if 500 <= response.status_code <= 511:
58+
span.set_tag("error", True)
59+
ec = span.tags.get('ec', 0)
60+
if ec is 0:
61+
span.set_tag("ec", ec+1)
62+
63+
span.set_tag(ext.HTTP_STATUS_CODE, int(response.status_code))
64+
tracer.inject(scope.span.context, opentracing.Format.HTTP_HEADERS, response.headers)
65+
response.headers.add('Server-Timing', "intid;desc=%s" % scope.span.context.trace_id)
66+
except:
67+
logger.debug("Flask after_request", exc_info=True)
68+
finally:
69+
if scope is not None:
70+
scope.close()
71+
return response
72+
73+
74+
@wrapt.patch_function_wrapper('flask', 'Flask.handle_user_exception')
75+
def handle_user_exception_with_instana(wrapped, instance, argv, kwargs):
76+
exc = argv[0]
77+
78+
if hasattr(flask.g, 'scope'):
79+
scope = flask.g.scope
80+
span = scope.span
81+
82+
if not hasattr(exc, 'code'):
83+
span.log_exception(argv[0])
84+
span.set_tag(ext.HTTP_STATUS_CODE, 500)
85+
scope.close()
86+
87+
return wrapped(*argv, **kwargs)
88+
89+
90+
@wrapt.patch_function_wrapper('flask', 'templating._render')
91+
def render_with_instana(wrapped, instance, argv, kwargs):
92+
ctx = argv[1]
93+
94+
# If we're not tracing, just return
95+
if not hasattr(ctx['g'], 'scope'):
96+
return wrapped(*argv, **kwargs)
97+
98+
with tracer.start_active_span("render", child_of=ctx['g'].scope.span) as rscope:
99+
try:
100+
template = argv[0]
101+
102+
rscope.span.set_tag("type", "template")
103+
if template.name is None:
104+
rscope.span.set_tag("name", '(from string)')
105+
else:
106+
rscope.span.set_tag("name", template.name)
107+
return wrapped(*argv, **kwargs)
108+
except Exception as e:
109+
rscope.span.log_exception(e)
110+
raise
111+
112+
113+
@wrapt.patch_function_wrapper('flask', 'Flask.full_dispatch_request')
114+
def full_dispatch_request_with_instana(wrapped, instance, argv, kwargs):
115+
if not hasattr(instance, '_stan_wuz_here'):
116+
logger.debug("Applying flask before/after instrumentation funcs")
117+
setattr(instance, "_stan_wuz_here", True)
118+
instance.after_request(after_request_with_instana)
119+
instance.before_request(before_request_with_instana)
120+
return wrapped(*argv, **kwargs)
121+
122+
123+
logger.debug("Instrumenting flask (without blinker support)")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
from __future__ import absolute_import
2+
3+
import opentracing
4+
import opentracing.ext.tags as ext
5+
import wrapt
6+
7+
from ...log import logger
8+
from ...singletons import agent, tracer
9+
from ...util import strip_secrets
10+
11+
import flask
12+
from flask import request_started, request_finished, got_request_exception
13+
14+
15+
def request_started_with_instana(sender, **extra):
16+
try:
17+
env = flask.request.environ
18+
ctx = None
19+
20+
if 'HTTP_X_INSTANA_T' in env and 'HTTP_X_INSTANA_S' in env:
21+
ctx = tracer.extract(opentracing.Format.HTTP_HEADERS, env)
22+
23+
flask.g.scope = tracer.start_active_span('wsgi', child_of=ctx)
24+
span = flask.g.scope.span
25+
26+
if agent.extra_headers is not None:
27+
for custom_header in agent.extra_headers:
28+
# Headers are available in this format: HTTP_X_CAPTURE_THIS
29+
header = ('HTTP_' + custom_header.upper()).replace('-', '_')
30+
if header in env:
31+
span.set_tag("http.%s" % custom_header, env[header])
32+
33+
span.set_tag(ext.HTTP_METHOD, flask.request.method)
34+
if 'PATH_INFO' in env:
35+
span.set_tag(ext.HTTP_URL, env['PATH_INFO'])
36+
if 'QUERY_STRING' in env and len(env['QUERY_STRING']):
37+
scrubbed_params = strip_secrets(env['QUERY_STRING'], agent.secrets_matcher, agent.secrets_list)
38+
span.set_tag("http.params", scrubbed_params)
39+
if 'HTTP_HOST' in env:
40+
span.set_tag("http.host", env['HTTP_HOST'])
41+
except:
42+
logger.debug("Flask before_request", exc_info=True)
43+
44+
45+
def request_finished_with_instana(sender, response, **extra):
46+
try:
47+
scope = None
48+
49+
# If we're not tracing, just return
50+
if not hasattr(flask.g, 'scope'):
51+
return
52+
53+
scope = flask.g.scope
54+
span = scope.span
55+
56+
if 500 <= response.status_code <= 511:
57+
span.set_tag("error", True)
58+
ec = span.tags.get('ec', 0)
59+
if ec is 0:
60+
span.set_tag("ec", ec+1)
61+
62+
span.set_tag(ext.HTTP_STATUS_CODE, int(response.status_code))
63+
tracer.inject(scope.span.context, opentracing.Format.HTTP_HEADERS, response.headers)
64+
response.headers.add('Server-Timing', "intid;desc=%s" % scope.span.context.trace_id)
65+
except:
66+
logger.debug("Flask after_request", exc_info=True)
67+
finally:
68+
if scope is not None:
69+
scope.close()
70+
return response
71+
72+
73+
def log_exception_with_instana(sender, exception, **extra):
74+
# If we're not tracing, just return
75+
if not hasattr(flask.g, 'scope'):
76+
return
77+
78+
scope = flask.g.scope
79+
80+
if scope is not None:
81+
span = scope.span
82+
if span is not None:
83+
span.log_exception(exception)
84+
85+
86+
@wrapt.patch_function_wrapper('flask', 'Flask.handle_user_exception')
87+
def handle_user_exception_with_instana(wrapped, instance, argv, kwargs):
88+
exc = argv[0]
89+
90+
if hasattr(flask.g, 'scope'):
91+
scope = flask.g.scope
92+
span = scope.span
93+
94+
if not hasattr(exc, 'code'):
95+
span.log_exception(exc)
96+
span.set_tag(ext.HTTP_STATUS_CODE, 500)
97+
scope.close()
98+
flask.g.scope = None
99+
100+
return wrapped(*argv, **kwargs)
101+
102+
103+
@wrapt.patch_function_wrapper('flask', 'templating._render')
104+
def render_with_instana(wrapped, instance, argv, kwargs):
105+
ctx = argv[1]
106+
107+
# If we're not tracing, just return
108+
if not hasattr(ctx['g'], 'scope'):
109+
return wrapped(*argv, **kwargs)
110+
111+
with tracer.start_active_span("render", child_of=ctx['g'].scope.span) as rscope:
112+
try:
113+
template = argv[0]
114+
115+
rscope.span.set_tag("type", "template")
116+
if template.name is None:
117+
rscope.span.set_tag("name", '(from string)')
118+
else:
119+
rscope.span.set_tag("name", template.name)
120+
return wrapped(*argv, **kwargs)
121+
except Exception as e:
122+
rscope.span.log_exception(e)
123+
raise
124+
125+
126+
@wrapt.patch_function_wrapper('flask', 'Flask.full_dispatch_request')
127+
def full_dispatch_request_with_instana(wrapped, instance, argv, kwargs):
128+
if not hasattr(instance, '_stan_wuz_here'):
129+
logger.debug("Applying flask before/after instrumentation funcs")
130+
setattr(instance, "_stan_wuz_here", True)
131+
got_request_exception.connect(log_exception_with_instana, instance)
132+
request_started.connect(request_started_with_instana, instance)
133+
request_finished.connect(request_finished_with_instana, instance)
134+
return wrapped(*argv, **kwargs)
135+
136+
137+
logger.debug("Instrumenting flask (with blinker support)")

instana/instrumentation/urllib3.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@
99
from ..util import strip_secrets
1010

1111
try:
12-
import urllib3 # noqa
12+
import urllib3
1313

1414
def collect(instance, args, kwargs):
1515
""" Build and return a fully qualified URL for this request """
1616
try:
17-
kvs = {}
17+
kvs = dict()
1818
kvs['host'] = instance.host
1919
kvs['port'] = instance.port
2020

@@ -28,7 +28,7 @@ def collect(instance, args, kwargs):
2828
kvs['path'] = kwargs.get('url')
2929

3030
# Strip any secrets from potential query params
31-
if '?' in kvs['path']:
31+
if kvs.get('path') is not None and ('?' in kvs['path']):
3232
parts = kvs['path'].split('?')
3333
kvs['path'] = parts[0]
3434
if len(parts) is 2:

instana/json_span.py

+20
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ class Data(BaseSpan):
3535
baggage = None
3636
custom = None
3737
http = None
38+
log = None
3839
rabbitmq = None
3940
redis = None
4041
rpc = None
42+
render = None
4143
sdk = None
4244
service = None
4345
sqlalchemy = None
@@ -56,6 +58,14 @@ class HttpData(BaseSpan):
5658
error = None
5759

5860

61+
class LogData(object):
62+
message = None
63+
parameters = None
64+
65+
def __init__(self, **kwds):
66+
self.__dict__.update(kwds)
67+
68+
5969
class MySQLData(BaseSpan):
6070
db = None
6171
host = None
@@ -91,6 +101,16 @@ class RPCData(BaseSpan):
91101
error = None
92102

93103

104+
class RenderData(object):
105+
type = None
106+
name = None
107+
message = None
108+
parameters = None
109+
110+
def __init__(self, **kwds):
111+
self.__dict__.update(kwds)
112+
113+
94114
class SQLAlchemyData(BaseSpan):
95115
sql = None
96116
url = None

0 commit comments

Comments
 (0)