Skip to content

Commit abac9cb

Browse files
authored
suds-jurko instrumentation (#75)
* Add background Soap server to test suite. * Better exception capture & logging in metric thread * Initial suds-jurko instrumentation * Basic Soap request tests. * Add SoapData section; Add soap as registered span * Cleanup and organize package dependencies * New test dependencies for Travis * Version sensitive class & method * Add requests to test bundle * Remove unicode characters; silence spyne logging * Exception & fault logging plus tests * Move exception logging out to span * Python 2 <-> 3 compatibility change * Moar HTTP tags. * Remove debug remnant and fix logger call
1 parent d64d14d commit abac9cb

14 files changed

+407
-50
lines changed

.travis.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ python:
55
- "3.4"
66
- "3.5"
77
- "3.6"
8-
install: "pip install -r test_requirements.txt"
8+
install: "pip install -r requirements-test.txt"
99
script: nosetests -v

instana/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
# Import & initialize instrumentation
1010
# noqa: ignore=W0611
1111
from .instrumentation import urllib3 # noqa
12+
from .instrumentation import sudsjurko # noqa
1213

1314
"""
1415
The Instana package has two core components: the sensor and the tracer.

instana/instrumentation/sudsjurko.py

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from __future__ import absolute_import
2+
import instana
3+
from instana.log import logger
4+
import opentracing
5+
import opentracing.ext.tags as ext
6+
import wrapt
7+
8+
9+
try:
10+
import suds # noqa
11+
12+
if (suds.version.__version__ <= '0.6'):
13+
class_method = 'SoapClient.send'
14+
else:
15+
class_method = '_SoapClient.send'
16+
17+
@wrapt.patch_function_wrapper('suds.client', class_method)
18+
def send_with_instana(wrapped, instance, args, kwargs):
19+
context = instana.internal_tracer.current_context()
20+
21+
# If we're not tracing, just return
22+
if context is None:
23+
return wrapped(*args, **kwargs)
24+
25+
try:
26+
span = instana.internal_tracer.start_span("soap", child_of=context)
27+
span.set_tag('soap.action', instance.method.name)
28+
span.set_tag(ext.HTTP_URL, instance.method.location)
29+
span.set_tag(ext.HTTP_METHOD, 'POST')
30+
31+
instana.internal_tracer.inject(span.context, opentracing.Format.HTTP_HEADERS,
32+
instance.options.headers)
33+
34+
rv = wrapped(*args, **kwargs)
35+
36+
except Exception as e:
37+
span.log_exception(e)
38+
span.set_tag(ext.HTTP_STATUS_CODE, 500)
39+
raise
40+
else:
41+
span.set_tag(ext.HTTP_STATUS_CODE, 200)
42+
return rv
43+
finally:
44+
span.finish()
45+
46+
logger.debug("Instrumenting suds-jurko")
47+
except ImportError:
48+
pass

instana/json_span.py

+7
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class Data(object):
2222
baggage = None
2323
custom = None
2424
sdk = None
25+
soap = None
2526

2627
def __init__(self, **kwds):
2728
self.__dict__.update(kwds)
@@ -36,6 +37,12 @@ class HttpData(object):
3637
def __init__(self, **kwds):
3738
self.__dict__.update(kwds)
3839

40+
class SoapData(object):
41+
action = None
42+
43+
def __init__(self, **kwds):
44+
self.__dict__.update(kwds)
45+
3946

4047
class CustomData(object):
4148
tags = None

instana/meter.py

+7-10
Original file line numberDiff line numberDiff line change
@@ -153,12 +153,10 @@ def collect_snapshot(self):
153153
s = Snapshot(name=appname, version=sys.version)
154154
s.version = sys.version
155155
s.versions = self.collect_modules()
156-
157-
return s
158156
except Exception as e:
159-
log.debug("collect_snapshot: ", str(e))
160-
161-
return None
157+
log.debug(e.message)
158+
else:
159+
return s
162160

163161
def jsonable(self, value):
164162
try:
@@ -174,8 +172,8 @@ def jsonable(self, value):
174172

175173
def collect_modules(self):
176174
try:
177-
m = sys.modules
178175
r = {}
176+
m = sys.modules
179177
for k in m:
180178
# Don't report submodules (e.g. django.x, django.y, django.z)
181179
if ('.' in k):
@@ -193,11 +191,10 @@ def collect_modules(self):
193191
r[k] = "unknown"
194192
log.debug("collect_modules: could not process module ", k, str(e))
195193

196-
return r
197194
except Exception as e:
198-
log.debug("collect_modules: ", str(e))
199-
200-
return None
195+
log.debug(e.message)
196+
else:
197+
return r
201198

202199
def collect_metrics(self):
203200
u = resource.getrusage(resource.RUSAGE_SELF)

instana/recorder.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import opentracing.ext.tags as ext
88
from basictracer import Sampler, SpanRecorder
9-
from .json_span import CustomData, Data, HttpData, JsonSpan, SDKData
9+
from .json_span import CustomData, Data, HttpData, SoapData, JsonSpan, SDKData
1010
from .agent_const import AGENT_TRACES_URL
1111

1212
import sys
@@ -18,9 +18,10 @@
1818

1919
class InstanaRecorder(SpanRecorder):
2020
sensor = None
21-
registered_spans = ("django", "memcache", "rpc-client", "rpc-server", "urllib3", "wsgi")
21+
registered_spans = ("django", "memcache", "rpc-client", "rpc-server",
22+
"soap", "urllib3", "wsgi")
2223
entry_kind = ["entry", "server", "consumer"]
23-
exit_kind = ["exit", "client", "producer"]
24+
exit_kind = ["exit", "client", "producer", "soap"]
2425
queue = queue.Queue()
2526

2627
def __init__(self, sensor):
@@ -84,9 +85,11 @@ def build_registered_span(self, span):
8485
url=self.get_string_tag(span, ext.HTTP_URL),
8586
method=self.get_string_tag(span, ext.HTTP_METHOD),
8687
status=self.get_tag(span, ext.HTTP_STATUS_CODE)),
88+
soap=SoapData(action=self.get_tag(span, 'soap.action')),
8789
baggage=span.context.baggage,
8890
custom=CustomData(tags=span.tags,
8991
logs=self.collect_logs(span)))
92+
9093
entityFrom = {'e': self.sensor.agent.from_.pid,
9194
'h': self.sensor.agent.from_.agentUuid}
9295

@@ -124,6 +127,8 @@ def build_sdk_span(self, span):
124127
d=int(round(span.duration * 1000)),
125128
n="sdk",
126129
f=entityFrom,
130+
# ec=self.get_tag(span, "ec"),
131+
# error=self.get_tag(span, "error"),
127132
data=data)
128133

129134
def get_tag(self, span, tag):

instana/span.py

+12
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,15 @@ def finish(self, finish_time=None):
1414
sampled=True)
1515
self.tracer.cur_ctx = pctx
1616
super(InstanaSpan, self).finish(finish_time)
17+
18+
def log_exception(self, e):
19+
if hasattr(e, 'message'):
20+
self.log_kv({'message': e.message})
21+
elif hasattr(e, '__str__'):
22+
self.log_kv({'message': e.__str__()})
23+
else:
24+
self.log_kv({'message': str(e)})
25+
26+
self.set_tag("error", True)
27+
ec = self.tags.get('ec', 0)
28+
self.set_tag("ec", ec+1)

requirements-test.txt

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# See setup.py for dependencies
2+
-e .[test]

requirements.txt

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,2 @@
1-
fysom>=2.1.2
2-
opentracing>=1.2.1
3-
basictracer>=2.2.0
4-
autowrapt>=1.0
1+
# See setup.py for dependencies
2+
-e .

setup.py

+31-22
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,37 @@
11
from setuptools import setup, find_packages
22

33
setup(name='instana',
4-
version='0.7.12',
5-
download_url='https://github.com/instana/python-sensor',
6-
url='https://www.instana.com/',
7-
license='MIT',
8-
author='Instana Inc.',
9-
author_email='[email protected]',
10-
description='Metrics sensor and trace collector for Instana',
11-
packages=find_packages(exclude=['tests', 'examples']),
12-
long_description="The instana package provides Python metrics and traces for Instana.",
13-
zip_safe=False,
14-
setup_requires=['nose>=1.0', 'flask>=0.12.2'],
15-
install_requires=['autowrapt>=1.0',
16-
'fysom>=2.1.2',
17-
'opentracing>=1.2.1,<1.3',
18-
'basictracer>=2.2.0'],
19-
entry_points={'django': ['django.core.handlers.base = instana.django:hook'],
20-
'django19': ['django.core.handlers.base = instana.django:hook19'],
21-
'flask': ['flask = instana.flaskana:hook'],
22-
'runtime': ['string = instana.runtime:hook']},
23-
test_suite='nose.collector',
24-
keywords=['performance', 'opentracing', 'metrics', 'monitoring'],
25-
classifiers=[
4+
version='0.7.12',
5+
download_url='https://github.com/instana/python-sensor',
6+
url='https://www.instana.com/',
7+
license='MIT',
8+
author='Instana Inc.',
9+
author_email='[email protected]',
10+
description='Metrics sensor and trace collector for Instana',
11+
packages=find_packages(exclude=['tests', 'examples']),
12+
long_description="The instana package provides Python metrics and traces for Instana.",
13+
zip_safe=False,
14+
install_requires=['autowrapt>=1.0',
15+
'fysom>=2.1.2',
16+
'opentracing>=1.2.1,<1.3',
17+
'basictracer>=2.2.0'],
18+
entry_points={'django': ['django.core.handlers.base = instana.django:hook'],
19+
'django19': ['django.core.handlers.base = instana.django:hook19'],
20+
'flask': ['flask = instana.flaskana:hook'],
21+
'runtime': ['string = instana.runtime:hook']},
22+
extras_require={
23+
'test': [
24+
'nose>=1.0',
25+
'flask>=0.12.2',
26+
'requests>=2.17.1',
27+
'spyne>=2.9',
28+
'lxml>=3.4',
29+
'suds-jurko>=0.6'
30+
],
31+
},
32+
test_suite='nose.collector',
33+
keywords=['performance', 'opentracing', 'metrics', 'monitoring'],
34+
classifiers=[
2635
'Development Status :: 5 - Production/Stable',
2736
'Framework :: Django',
2837
'Framework :: Flask',

test_requirements.txt

-8
This file was deleted.

tests/__init__.py

+21-2
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,33 @@
33
import time
44
import threading
55
from .apps.flaskalino import app as flaskalino
6+
from .apps.soapserver4132 import soapserver
7+
68
os.environ["INSTANA_TEST"] = "true"
79

10+
11+
# Background Flask application
12+
#
813
# Spawn our background Flask app that the tests will throw
914
# requests at. Don't continue until the test app is fully
1015
# up and running.
1116
timer = threading.Thread(target=flaskalino.run)
1217
timer.daemon = True
13-
timer.name = "Test Flask app"
14-
print("Starting background test app")
18+
timer.name = "Background Flask app"
19+
print("Starting background Flask app...")
1520
timer.start()
21+
22+
23+
# Background Soap Server
24+
#
25+
# Spawn our background Flask app that the tests will throw
26+
# requests at. Don't continue until the test app is fully
27+
# up and running.
28+
timer = threading.Thread(target=soapserver.serve_forever)
29+
timer.daemon = True
30+
timer.name = "Background Soap server"
31+
print("Starting background Soap server...")
32+
timer.start()
33+
34+
1635
time.sleep(1)

tests/apps/soapserver4132.py

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# vim: set fileencoding=UTF-8 :
2+
import logging
3+
from spyne import Application, rpc, ServiceBase, Iterable, Integer, Unicode, Fault
4+
from spyne.protocol.soap import Soap11
5+
from spyne.server.wsgi import WsgiApplication
6+
from wsgiref.simple_server import make_server
7+
from instana.wsgi import iWSGIMiddleware
8+
9+
# Simple in test suite SOAP server to test suds client instrumentation against.
10+
# Configured to listen on localhost port 4132
11+
# WSDL: http://localhost:4232/?wsdl
12+
13+
class StanSoapService(ServiceBase):
14+
@rpc(Unicode, Integer, _returns=Iterable(Unicode))
15+
def ask_question(ctx, question, answer):
16+
"""Ask Stan a question!
17+
<b>Ask Stan questions as a Service</b>
18+
19+
@param name the name to say hello to
20+
@param times the number of times to say hello
21+
@return the completed array
22+
"""
23+
24+
yield u'To an artificial mind, all reality is virtual. How do they know that the real world isn\'t just another simulation? How do you?'
25+
26+
27+
@rpc()
28+
def server_exception(ctx):
29+
raise Exception("Server side exception example.")
30+
31+
@rpc()
32+
def server_fault(ctx):
33+
raise Fault("Server", "Server side fault example.")
34+
35+
@rpc()
36+
def client_fault(ctx):
37+
raise Fault("Client", "Client side fault example")
38+
39+
40+
41+
app = Application([StanSoapService], 'instana.tests.app.ask_question',
42+
in_protocol=Soap11(validator='lxml'), out_protocol=Soap11())
43+
44+
# Use Instana middleware so we can test context passing and Soap server traces.
45+
wsgi_app = iWSGIMiddleware(WsgiApplication(app))
46+
soapserver = make_server('127.0.0.1', 4132, wsgi_app)
47+
48+
logging.basicConfig(level=logging.WARN)
49+
logging.getLogger('suds').setLevel(logging.WARN)
50+
logging.getLogger('suds.resolver').setLevel(logging.WARN)
51+
logging.getLogger('spyne.protocol.xml').setLevel(logging.WARN)
52+
logging.getLogger('spyne.model.complex').setLevel(logging.WARN)
53+
54+
if __name__ == '__main__':
55+
soapserver.serve_forever()

0 commit comments

Comments
 (0)