Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flask Instrumentation through WSGI middleware. #8

Merged
merged 23 commits into from
Jul 27, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
94928a7
Better json encoding and fix error logging.
pglombardo Jul 25, 2017
c5c7491
Remove debug message
pglombardo Jul 25, 2017
e3b009a
Linter hapiness.
pglombardo Jul 25, 2017
5dfe863
Protect inject/extract with try/catch
pglombardo Jul 25, 2017
570bb1c
General WSGI instrumentation for many frameworks.
pglombardo Jul 25, 2017
5900385
Add Flask instructions to README
pglombardo Jul 25, 2017
f33b44f
Add wsgi to list of registered spans.
pglombardo Jul 25, 2017
da26d78
Fix formatting and remove unneeded lines
pglombardo Jul 25, 2017
b1393e4
Merge branch 'master' into flask
pglombardo Jul 26, 2017
24a28b1
Set flask hook point for autowrapt
pglombardo Jul 27, 2017
73ca3a4
Flask hook instrumentation
pglombardo Jul 27, 2017
b734575
Remove debug line
pglombardo Jul 27, 2017
0ddf327
Merge branch 'flask' of github.com:instana/python-sensor into flask
pglombardo Jul 27, 2017
9729bbc
Simplify env var hooks.
pglombardo Jul 27, 2017
e3be59b
Updated README per instrumentation changes.
pglombardo Jul 27, 2017
cafb615
Flask == Flask, not Django thx Fabian
pglombardo Jul 27, 2017
71ea5ba
Add runtime only hook & instrumentation
pglombardo Jul 27, 2017
0503814
Add check that we really want runtime only
pglombardo Jul 27, 2017
a731c3c
Add list type support to the HTTP propagator
pglombardo Jul 27, 2017
0e15586
Enable inject for response headers.
pglombardo Jul 27, 2017
949dd2c
Report a better appname
pglombardo Jul 27, 2017
f326fa4
Fix log message
pglombardo Jul 27, 2017
0e50715
Simplify and add list/tuple support for extract
pglombardo Jul 27, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,29 @@ Any and all feedback is welcome. Happy Python visibility.

## Installation

There are two steps required to install the the Instana package for your applications:
For this BETA, we currently support tracing of Django and Flask applications or optionally just runtime monitoring of your Python applications.

1. `pip install instana` into the virtual-env or container ([hosted on pypi](https://pypi.python.org/pypi/instana))
`pip install instana` into the virtual-env or container ([hosted on pypi](https://pypi.python.org/pypi/instana))

2. Enable instrumentation for the frameworks in use by setting an environment variable:
`AUTOWRAPT_BOOTSTRAP=instana.django`
## Django

To enable the Django instrumentation, set the following environment variable in your _application boot environment_ and then restart your application:

`export AUTOWRAPT_BOOTSTRAP=django`

## Flask

To enable the Flask instrumentation, set the following environment variable in your _application boot environment_ and then restart your application:

`export AUTOWRAPT_BOOTSTRAP=flask`

## Runtime Monitoring Only

_Note: When the Django or Flask instrumentation is used, runtime monitoring is automatically included. Use this section if you only want to see runtime metrics._

To enable runtime monitoring (without request tracing), set the following environment variable in your _application boot environment_ and then restart your application:

`export AUTOWRAPT_BOOTSTRAP=runtime`

## Usage

Expand Down
1 change: 0 additions & 1 deletion instana/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ def full_request_response(self, url, method, o, body, header):
request = urllib2.Request(url, self.to_json(o))
request.add_header("Content-Type", "application/json")

l.debug("request: ", method, self.to_json(o))
response = urllib2.urlopen(request, timeout=2)

if not response:
Expand Down
15 changes: 15 additions & 0 deletions instana/flaskana.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from instana import wsgi
from flask.cli import ScriptInfo


def wrap_load_app(func):
def wrapper(self, *args):
app = func(self, *args)
app.wsgi_app = wsgi.iWSGIMiddleware(app.wsgi_app)
return app
return wrapper


def hook(module):
""" Hook method to install the Instana middleware into Flask """
ScriptInfo.load_app = wrap_load_app(ScriptInfo.load_app)
7 changes: 6 additions & 1 deletion instana/meter.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,12 @@ def process(self):

def collect_snapshot(self):
try:
s = Snapshot(name=self.sensor.service_name,
if "FLASK_APP" in os.environ:
appname = os.environ["FLASK_APP"]
else:
appname = os.path.basename(sys.executable)

s = Snapshot(name=appname,
version=sys.version,
rlimit_core=resource.getrlimit(resource.RLIMIT_CORE),
rlimit_cpu=resource.getrlimit(resource.RLIMIT_CPU),
Expand Down
60 changes: 37 additions & 23 deletions instana/propagator.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import absolute_import
import opentracing as ot
from instana import util
from basictracer.context import SpanContext
from instana import util, log

prefix_tracer_state = 'HTTP_X_INSTANA_'
field_name_trace_id = prefix_tracer_state + 'T'
Expand All @@ -12,27 +13,40 @@ class HTTPPropagator():
"""A Propagator for Format.HTTP_HEADERS. """

def inject(self, span_context, carrier):
carrier[field_name_trace_id] = util.id_to_header(span_context.trace_id)
carrier[field_name_span_id] = util.id_to_header(span_context.span_id)
try:
trace_id = util.id_to_header(span_context.trace_id)
span_id = util.id_to_header(span_context.span_id)
if type(carrier) is dict:
carrier[field_name_trace_id] = trace_id
carrier[field_name_span_id] = span_id
elif type(carrier) is list:
trace_header = (field_name_trace_id, trace_id)
carrier.append(trace_header)
span_header = (field_name_span_id, span_id)
carrier.append(span_header)
else:
raise Exception("Unsupported carrier type", type(carrier))

except Exception as e:
log.debug("inject error: ", str(e))

def extract(self, carrier): # noqa
count = 0
span_id, trace_id = (0, 0)
for k in carrier:
v = carrier[k]
k = k.lower()
if k == field_name_span_id:
span_id = util.header_to_id(v)
count += 1
elif k == field_name_trace_id:
trace_id = util.header_to_id(v)
count += 1

if count != field_count:
raise ot.SpanContextCorruptedException()

return ot.SpanContext(
span_id=span_id,
trace_id=trace_id,
baggage={},
sampled=True)
try:
if type(carrier) is dict:
dc = carrier
elif type(carrier) is list:
dc = dict(carrier)
else:
raise ot.SpanContextCorruptedException()

if field_name_trace_id in dc and field_name_span_id in dc:
trace_id = util.header_to_id(dc[field_name_trace_id])
span_id = util.header_to_id(dc[field_name_span_id])

return SpanContext(span_id=span_id,
trace_id=trace_id,
baggage={},
sampled=True)

except Exception as e:
log.debug("extract error: ", str(e))
2 changes: 1 addition & 1 deletion instana/recorder.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

class InstanaRecorder(SpanRecorder):
sensor = None
registered_spans = ("django", "memcache", "rpc-client", "rpc-server")
registered_spans = ("django", "memcache", "rpc-client", "rpc-server", "wsgi")
entry_kind = ["entry", "server", "consumer"]
exit_kind = ["exit", "client", "producer"]
queue = queue.Queue()
Expand Down
15 changes: 15 additions & 0 deletions instana/runtime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import opentracing as ot
from instana import tracer, options
import logging
import os


def hook(module):
""" Hook method to install the Instana middleware into Flask """
if os.environ["AUTOWRAPT_BOOTSTRAP"] == "runtime":
if "INSTANA_DEV" in os.environ:
level = logging.DEBUG
else:
level = logging.WARN
opts = options.Options(log_level=level)
ot.global_tracer = tracer.InstanaTracer(opts)
4 changes: 4 additions & 0 deletions instana/span.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class InstanaSpan(object):
def __init__(self, **kwds):
self.__dict__.update(kwds)


class Data(object):
service = None
http = None
Expand All @@ -26,6 +27,7 @@ class Data(object):
def __init__(self, **kwds):
self.__dict__.update(kwds)


class HttpData(object):
host = None
url = None
Expand All @@ -35,13 +37,15 @@ class HttpData(object):
def __init__(self, **kwds):
self.__dict__.update(kwds)


class CustomData(object):
tags = None
logs = None

def __init__(self, **kwds):
self.__dict__.update(kwds)


class SDKData(object):
name = None
Type = None
Expand Down
45 changes: 45 additions & 0 deletions instana/wsgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from __future__ import print_function
import opentracing as ot
from instana import tracer, options
import opentracing.ext.tags as ext
import logging


class iWSGIMiddleware(object):
""" Instana WSGI middleware """

def __init__(self, app):
self.app = app
opts = options.Options(log_level=logging.DEBUG)
ot.global_tracer = tracer.InstanaTracer(opts)
self

def __call__(self, environ, start_response):
env = environ

def new_start_response(status, headers, exc_info=None):
"""Modified start response with additional headers."""
ot.global_tracer.inject(span.context, ot.Format.HTTP_HEADERS, headers)
res = start_response(status, headers, exc_info)
span.set_tag(ext.HTTP_STATUS_CODE, status.split(' ')[0])
span.finish()
return res

if 'HTTP_X_INSTANA_T' in env and 'HTTP_X_INSTANA_S' in env:
ctx = ot.global_tracer.extract(ot.Format.HTTP_HEADERS, env)
span = ot.global_tracer.start_span("wsgi", child_of=ctx)
else:
span = ot.global_tracer.start_span("wsgi")

span.set_tag(ext.HTTP_URL, env['PATH_INFO'])
span.set_tag("http.params", env['QUERY_STRING'])
span.set_tag(ext.HTTP_METHOD, env['REQUEST_METHOD'])
span.set_tag("http.host", env['HTTP_HOST'])

return self.app(environ, new_start_response)


def make_middleware(app=None, *args, **kw):
""" Given an app, return that app wrapped in iWSGIMiddleware """
app = iWSGIMiddleware(app, *args, **kw)
return app
4 changes: 3 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
'opentracing>=1.2.1,<1.3',
'basictracer>=2.2.0',
'psutil>=5.1.3'],
entry_points={'instana.django': ['django.core.handlers.base = instana.django:hook']},
entry_points={'django': ['django.core.handlers.base = instana.django:hook'],
'flask': ['flask.cli = instana.flaskana:hook'],
'runtime': ['string = instana.runtime:hook']},
test_suite='nose.collector',
keywords=['performance', 'opentracing', 'metrics', 'monitoring'],
classifiers=[
Expand Down