Skip to content

Commit f05f7a9

Browse files
authored
Support Custom Header collection (#99)
* Mr. Linter * Store and collect custom headers in WSGI and Django middleware * Assure params are there before reporting. * Tests to validate custom header capture * Use dict methods instead of our own * a more precise function name
1 parent 1733725 commit f05f7a9

File tree

7 files changed

+146
-36
lines changed

7 files changed

+146
-36
lines changed

instana/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def load(module):
5555

5656
import instana.singletons #noqa
5757

58+
5859
def load_instrumentation():
5960
if "INSTANA_DISABLE_AUTO_INSTR" not in os.environ:
6061
# Import & initialize instrumentation
@@ -63,6 +64,7 @@ def load_instrumentation():
6364
from .instrumentation import mysqlpython # noqa
6465
from .instrumentation.django import middleware # noqa
6566

67+
6668
if "INSTANA_MAGIC" in os.environ:
6769
# If we're being loaded into an already running process, then delay
6870
# instrumentation load.

instana/agent.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class Agent(object):
4747
last_seen = None
4848
last_fork_check = None
4949
_boot_pid = os.getpid()
50+
extra_headers = None
5051

5152
def __init__(self):
5253
logger.debug("initializing agent")
@@ -165,7 +166,13 @@ def set_from(self, json_string):
165166
else:
166167
raw_json = json_string
167168

168-
self.from_ = From(**json.loads(raw_json))
169+
res_data = json.loads(raw_json)
170+
171+
if "extraHeaders" in res_data:
172+
self.extra_headers = res_data['extraHeaders']
173+
logger.debug("Will also capture these custom headers: %s", self.extra_headers)
174+
175+
self.from_ = From(pid=res_data['pid'], agentUuid=res_data['agentUuid'])
169176

170177
def reset(self):
171178
self.last_seen = None

instana/instrumentation/django/middleware.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,17 @@ def process_request(self, request):
3333

3434
request.iscope = tracer.start_active_span('django', child_of=ctx)
3535

36+
if agent.extra_headers is not None:
37+
for custom_header in agent.extra_headers:
38+
# Headers are available in this format: HTTP_X_CAPTURE_THIS
39+
django_header = ('HTTP_' + custom_header.upper()).replace('-', '_')
40+
if django_header in env:
41+
request.iscope.span.set_tag("http.%s" % custom_header, env[django_header])
42+
3643
request.iscope.span.set_tag(ext.HTTP_METHOD, request.method)
3744
if 'PATH_INFO' in env:
3845
request.iscope.span.set_tag(ext.HTTP_URL, env['PATH_INFO'])
39-
if 'QUERY_STRING' in env:
46+
if 'QUERY_STRING' in env and len(env['QUERY_STRING']):
4047
request.iscope.span.set_tag("http.params", env['QUERY_STRING'])
4148
if 'HTTP_HOST' in env:
4249
request.iscope.span.set_tag("http.host", env['HTTP_HOST'])

instana/recorder.py

Lines changed: 19 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,11 @@ def report_spans(self):
4444
""" Periodically report the queued spans """
4545
logger.debug("Span reporting thread is now alive")
4646
while 1:
47-
if self.queue.qsize() > 0 and instana.singletons.agent.can_send():
47+
queue_size = self.queue.qsize()
48+
if queue_size > 0 and instana.singletons.agent.can_send():
4849
url = instana.singletons.agent.make_url(AGENT_TRACES_URL)
4950
instana.singletons.agent.request(url, "POST", self.queued_spans())
51+
logger.debug("reported %d spans" % queue_size)
5052
time.sleep(1)
5153

5254
def queue_size(self):
@@ -91,20 +93,20 @@ def build_registered_span(self, span):
9193
logs=self.collect_logs(span)))
9294

9395
if span.operation_name in self.http_spans:
94-
data.http = HttpData(host=self.get_host_name(span),
95-
url=self.get_string_tag(span, ext.HTTP_URL),
96-
method=self.get_string_tag(span, ext.HTTP_METHOD),
97-
status=self.get_tag(span, ext.HTTP_STATUS_CODE),
98-
error=self.get_tag(span, 'http.error'))
96+
data.http = HttpData(host=self.get_http_host_name(span),
97+
url=span.tags.pop(ext.HTTP_URL, ""),
98+
method=span.tags.pop(ext.HTTP_METHOD, ""),
99+
status=span.tags.pop(ext.HTTP_STATUS_CODE, None),
100+
error=span.tags.pop('http.error', None))
99101

100102
if span.operation_name == "soap":
101-
data.soap = SoapData(action=self.get_tag(span, 'soap.action'))
103+
data.soap = SoapData(action=span.tags.pop('soap.action', None))
102104

103105
if span.operation_name == "mysql":
104-
data.mysql = MySQLData(host=self.get_tag(span, 'host'),
105-
db=self.get_tag(span, ext.DATABASE_INSTANCE),
106-
user=self.get_tag(span, ext.DATABASE_USER),
107-
stmt=self.get_tag(span, ext.DATABASE_STATEMENT))
106+
data.mysql = MySQLData(host=span.tags.pop('host', None),
107+
db=span.tags.pop(ext.DATABASE_INSTANCE, None),
108+
user=span.tags.pop(ext.DATABASE_USER, None),
109+
stmt=span.tags.pop(ext.DATABASE_STATEMENT, None))
108110
if len(data.custom.logs.keys()):
109111
tskey = list(data.custom.logs.keys())[0]
110112
data.mysql.error = data.custom.logs[tskey]['message']
@@ -121,8 +123,8 @@ def build_registered_span(self, span):
121123
f=entityFrom,
122124
data=data)
123125

124-
error = self.get_tag(span, "error", False)
125-
ec = self.get_tag(span, "ec", None)
126+
error = span.tags.pop("error", False)
127+
ec = span.tags.pop("ec", None)
126128

127129
if error and ec:
128130
json_span.error = error
@@ -154,30 +156,17 @@ def build_sdk_span(self, span):
154156
f=entityFrom,
155157
data=data)
156158

157-
error = self.get_tag(span, "error", False)
158-
ec = self.get_tag(span, "ec", None)
159+
error = span.tags.pop("error", False)
160+
ec = span.tags.pop("ec", None)
159161

160162
if error and ec:
161163
json_span.error = error
162164
json_span.ec = ec
163165

164166
return json_span
165167

166-
def get_tag(self, span, tag, default=None):
167-
if tag in span.tags:
168-
return span.tags[tag]
169-
170-
return default
171-
172-
def get_string_tag(self, span, tag):
173-
ret = self.get_tag(span, tag)
174-
if not ret:
175-
return ""
176-
177-
return ret
178-
179-
def get_host_name(self, span):
180-
h = self.get_string_tag(span, "http.host")
168+
def get_http_host_name(self, span):
169+
h = span.tags.pop("http.host", "")
181170
if len(h) > 0:
182171
return h
183172

instana/wsgi.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import opentracing as ot
44
import opentracing.ext.tags as tags
55

6-
from .singletons import tracer
6+
from .singletons import agent, tracer
77

88

99
class iWSGIMiddleware(object):
@@ -37,9 +37,17 @@ def new_start_response(status, headers, exc_info=None):
3737

3838
self.scope = tracer.start_active_span("wsgi", child_of=ctx)
3939

40+
if agent.extra_headers is not None:
41+
for custom_header in agent.extra_headers:
42+
# Headers are available in this format: HTTP_X_CAPTURE_THIS
43+
wsgi_header = ('HTTP_' + custom_header.upper()).replace('-', '_')
44+
if wsgi_header in env:
45+
self.scope.span.set_tag("http.%s" % custom_header, env[wsgi_header])
46+
47+
4048
if 'PATH_INFO' in env:
4149
self.scope.span.set_tag(tags.HTTP_URL, env['PATH_INFO'])
42-
if 'QUERY_STRING' in env:
50+
if 'QUERY_STRING' in env and len(env['QUERY_STRING']):
4351
self.scope.span.set_tag("http.params", env['QUERY_STRING'])
4452
if 'REQUEST_METHOD' in env:
4553
self.scope.span.set_tag(tags.HTTP_METHOD, env['REQUEST_METHOD'])

tests/test_django.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
66
from nose.tools import assert_equals
77

8-
from instana.singletons import tracer
8+
from instana.singletons import agent, tracer
99

1010
from .apps.app_django import INSTALLED_APPS
1111

@@ -123,3 +123,48 @@ def test_complex_request(self):
123123
assert_equals('/complex', django_span.data.http.url)
124124
assert_equals('GET', django_span.data.http.method)
125125
assert_equals(200, django_span.data.http.status)
126+
127+
def test_custom_header_capture(self):
128+
# Hack together a manual custom headers list
129+
agent.extra_headers = [u'X-Capture-This', u'X-Capture-That']
130+
131+
request_headers = {}
132+
request_headers['X-Capture-This'] = 'this'
133+
request_headers['X-Capture-That'] = 'that'
134+
135+
with tracer.start_active_span('test'):
136+
response = self.http.request('GET', self.live_server_url + '/', headers=request_headers)
137+
# response = self.client.get('/')
138+
139+
assert_equals(response.status, 200)
140+
141+
spans = self.recorder.queued_spans()
142+
assert_equals(3, len(spans))
143+
144+
test_span = spans[2]
145+
urllib3_span = spans[1]
146+
django_span = spans[0]
147+
148+
# import ipdb; ipdb.set_trace()
149+
150+
assert_equals("test", test_span.data.sdk.name)
151+
assert_equals("urllib3", urllib3_span.n)
152+
assert_equals("django", django_span.n)
153+
154+
assert_equals(test_span.t, urllib3_span.t)
155+
assert_equals(urllib3_span.t, django_span.t)
156+
157+
assert_equals(urllib3_span.p, test_span.s)
158+
assert_equals(django_span.p, urllib3_span.s)
159+
160+
assert_equals(None, django_span.error)
161+
assert_equals(None, django_span.ec)
162+
163+
assert_equals('/', django_span.data.http.url)
164+
assert_equals('GET', django_span.data.http.method)
165+
assert_equals(200, django_span.data.http.status)
166+
167+
assert_equals(True, "http.X-Capture-This" in django_span.data.custom.__dict__['tags'])
168+
assert_equals("this", django_span.data.custom.__dict__['tags']["http.X-Capture-This"])
169+
assert_equals(True, "http.X-Capture-That" in django_span.data.custom.__dict__['tags'])
170+
assert_equals("that", django_span.data.custom.__dict__['tags']["http.X-Capture-That"])

tests/test_wsgi.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import unittest
55

66
import urllib3
7-
from instana.singletons import tracer
7+
from instana.singletons import agent, tracer
88

99

1010
class TestWSGI(unittest.TestCase):
@@ -116,3 +116,55 @@ def test_complex_request(self):
116116
self.assertEqual('GET', wsgi_span.data.http.method)
117117
self.assertEqual('200', wsgi_span.data.http.status)
118118
self.assertIsNone(wsgi_span.data.http.error)
119+
120+
def test_custom_header_capture(self):
121+
# Hack together a manual custom headers list
122+
agent.extra_headers = [u'X-Capture-This', u'X-Capture-That']
123+
124+
request_headers = {}
125+
request_headers['X-Capture-This'] = 'this'
126+
request_headers['X-Capture-That'] = 'that'
127+
128+
with tracer.start_active_span('test'):
129+
response = self.http.request('GET', 'http://127.0.0.1:5000/', headers=request_headers)
130+
131+
spans = self.recorder.queued_spans()
132+
133+
self.assertEqual(3, len(spans))
134+
self.assertIsNone(tracer.active_span)
135+
136+
wsgi_span = spans[0]
137+
urllib3_span = spans[1]
138+
test_span = spans[2]
139+
140+
assert(response)
141+
self.assertEqual(200, response.status)
142+
143+
# Same traceId
144+
self.assertEqual(test_span.t, urllib3_span.t)
145+
self.assertEqual(urllib3_span.t, wsgi_span.t)
146+
147+
# Parent relationships
148+
self.assertEqual(urllib3_span.p, test_span.s)
149+
self.assertEqual(wsgi_span.p, urllib3_span.s)
150+
151+
# Error logging
152+
self.assertFalse(test_span.error)
153+
self.assertIsNone(test_span.ec)
154+
self.assertFalse(urllib3_span.error)
155+
self.assertIsNone(urllib3_span.ec)
156+
self.assertFalse(wsgi_span.error)
157+
self.assertIsNone(wsgi_span.ec)
158+
159+
# wsgi
160+
self.assertEqual("wsgi", wsgi_span.n)
161+
self.assertEqual('127.0.0.1:5000', wsgi_span.data.http.host)
162+
self.assertEqual('/', wsgi_span.data.http.url)
163+
self.assertEqual('GET', wsgi_span.data.http.method)
164+
self.assertEqual('200', wsgi_span.data.http.status)
165+
self.assertIsNone(wsgi_span.data.http.error)
166+
167+
self.assertEqual(True, "http.X-Capture-This" in wsgi_span.data.custom.__dict__['tags'])
168+
self.assertEqual("this", wsgi_span.data.custom.__dict__['tags']["http.X-Capture-This"])
169+
self.assertEqual(True, "http.X-Capture-That" in wsgi_span.data.custom.__dict__['tags'])
170+
self.assertEqual("that", wsgi_span.data.custom.__dict__['tags']["http.X-Capture-That"])

0 commit comments

Comments
 (0)