Skip to content

Commit cc7273f

Browse files
authored
Use unsigned base 16 IDs internally & update tests (#119)
* Use Unsigned based 16 IDs internally & update tests * Add test for 128 bit incoming headers * Remove unused imports; Add function documentation * Fix assertion values
1 parent 5ca0ea0 commit cc7273f

File tree

8 files changed

+104
-156
lines changed

8 files changed

+104
-156
lines changed

instana/http_propagator.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from basictracer.context import SpanContext
55

66
from .log import logger
7-
from .util import id_to_header, header_to_id
7+
from .util import header_to_id
88

99
# The carrier can be a dict or a list.
1010
# Using the trace header as an example, it can be in the following forms
@@ -37,8 +37,8 @@ class HTTPPropagator():
3737

3838
def inject(self, span_context, carrier):
3939
try:
40-
trace_id = id_to_header(span_context.trace_id)
41-
span_id = id_to_header(span_context.span_id)
40+
trace_id = span_context.trace_id
41+
span_id = span_context.span_id
4242

4343
if type(carrier) is dict or hasattr(carrier, "__dict__"):
4444
carrier[self.HEADER_KEY_T] = trace_id

instana/text_propagator.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from basictracer.context import SpanContext
55

66
from .log import logger
7-
from .util import id_to_header, header_to_id
7+
from .util import header_to_id
88

99
prefix_tracer_state = 'X-INSTANA-'
1010
prefix_baggage = 'X-INSTANA-BAGGAGE-'
@@ -19,8 +19,8 @@ class TextPropagator():
1919

2020
def inject(self, span_context, carrier):
2121
try:
22-
carrier[field_name_trace_id] = '{0:x}'.format(span_context.trace_id)
23-
carrier[field_name_span_id] = '{0:x}'.format(span_context.span_id)
22+
carrier[field_name_trace_id] = span_context.trace_id
23+
carrier[field_name_span_id] = span_context.span_id
2424
if span_context.baggage is not None:
2525
for k in span_context.baggage:
2626
carrier[prefix_baggage+k] = span_context.baggage[k]

instana/util.py

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
import binascii
21
import json
32
import os
43
import random
54
import re
6-
import struct
75
import sys
86
import time
97

@@ -26,54 +24,62 @@
2624
_rnd = random.Random()
2725
_current_pid = 0
2826

29-
BAD_ID_LONG = 3135097598 # Bad Cafe in base 10
30-
BAD_ID_HEADER = "BADDCAFE" # Bad Cafe
27+
BAD_ID = "BADCAFFE" # Bad Caffe
3128

3229

3330
def generate_id():
34-
""" Generate a 64bit signed integer for use as a Span or Trace ID """
31+
""" Generate a 64bit base 16 ID for use as a Span or Trace ID """
3532
global _current_pid
3633

3734
pid = os.getpid()
3835
if _current_pid != pid:
3936
_current_pid = pid
4037
_rnd.seed(int(1000000 * time.time()) ^ pid)
41-
return _rnd.randint(-9223372036854775808, 9223372036854775807)
38+
id = format(_rnd.randint(0, 18446744073709551615), '02x')
4239

40+
if len(id) < 16:
41+
id = id.zfill(16)
4342

44-
def id_to_header(id):
45-
""" Convert a 64bit signed integer to an unsigned base 16 hex string """
46-
47-
try:
48-
if not isinstance(id, int):
49-
return BAD_ID_HEADER
50-
51-
byte_string = struct.pack('>q', id)
52-
return str(binascii.hexlify(byte_string).decode('UTF-8').lstrip('0'))
53-
except Exception as e:
54-
logger.debug(e)
55-
return BAD_ID_HEADER
43+
return id
5644

5745

5846
def header_to_id(header):
59-
""" Convert an unsigned base 16 hex string into a 64bit signed integer """
47+
"""
48+
We can receive headers in the following formats:
49+
1. unsigned base 16 hex string of variable length
50+
2. [eventual]
6051
52+
:param header: the header to analyze, validate and convert (if needed)
53+
:return: a valid ID to be used internal to the tracer
54+
"""
6155
if not isinstance(header, string_types):
62-
return BAD_ID_LONG
56+
return BAD_ID
6357

6458
try:
6559
# Test that header is truly a hexadecimal value before we try to convert
6660
int(header, 16)
6761

68-
# Pad the header to 16 chars
69-
header = header.zfill(16)
70-
r = binascii.unhexlify(header)
71-
return struct.unpack('>q', r)[0]
62+
length = len(header)
63+
if length < 16:
64+
# Left pad ID with zeros
65+
header = header.zfill(16)
66+
elif length > 16:
67+
# Phase 0: Discard everything but the last 16byte
68+
header = header[-16:]
69+
70+
return header
7271
except ValueError:
73-
return BAD_ID_LONG
72+
return BAD_ID
7473

7574

7675
def to_json(obj):
76+
"""
77+
Convert obj to json. Used mostly to convert the classes in json_span.py until we switch to nested
78+
dicts (or something better)
79+
80+
:param obj: the object to serialize to json
81+
:return: json string
82+
"""
7783
try:
7884
return json.dumps(obj, default=lambda obj: {k.lower(): v for k, v in obj.__dict__.items()},
7985
sort_keys=False, separators=(',', ':')).encode()
@@ -82,6 +88,11 @@ def to_json(obj):
8288

8389

8490
def package_version():
91+
"""
92+
Determine the version of this package.
93+
94+
:return: String representing known version
95+
"""
8596
version = ""
8697
try:
8798
version = pkg_resources.get_distribution('instana').version

tests/test_django.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ def test_with_incoming_context(self):
212212
assert_equals(200, response.status)
213213
assert('X-Instana-T' in response.headers)
214214
assert(int(response.headers['X-Instana-T'], 16))
215-
self.assertEqual('1', response.headers['X-Instana-T'])
215+
self.assertEqual('0000000000000001', response.headers['X-Instana-T'])
216216
assert('X-Instana-S' in response.headers)
217217
assert(int(response.headers['X-Instana-S'], 16))
218218
assert('X-Instana-L' in response.headers)
@@ -223,8 +223,8 @@ def test_with_incoming_context(self):
223223

224224
django_span = spans[0]
225225

226-
assert_equals(django_span.t, 1)
227-
assert_equals(django_span.p, 1)
226+
assert_equals(django_span.t, '0000000000000001')
227+
assert_equals(django_span.p, '0000000000000001')
228228

229229
def test_with_incoming_mixed_case_context(self):
230230
request_headers = dict()
@@ -237,7 +237,7 @@ def test_with_incoming_mixed_case_context(self):
237237
assert_equals(200, response.status)
238238
assert('X-Instana-T' in response.headers)
239239
assert(int(response.headers['X-Instana-T'], 16))
240-
self.assertEqual('1', response.headers['X-Instana-T'])
240+
self.assertEqual('0000000000000001', response.headers['X-Instana-T'])
241241
assert('X-Instana-S' in response.headers)
242242
assert(int(response.headers['X-Instana-S'], 16))
243243
assert('X-Instana-L' in response.headers)
@@ -248,5 +248,5 @@ def test_with_incoming_mixed_case_context(self):
248248

249249
django_span = spans[0]
250250

251-
assert_equals(django_span.t, 1)
252-
assert_equals(django_span.p, 1)
251+
assert_equals(django_span.t, '0000000000000001')
252+
assert_equals(django_span.p, '0000000000000001')

tests/test_id_management.py

Lines changed: 28 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -13,126 +13,49 @@
1313

1414
def test_id_generation():
1515
count = 0
16-
while count <= 1000:
16+
while count <= 10000:
1717
id = instana.util.generate_id()
18-
assert id >= -9223372036854775808
19-
assert id <= 9223372036854775807
18+
base10_id = int(id, 16)
19+
assert base10_id >= 0
20+
assert base10_id <= 18446744073709551615
2021
count += 1
2122

2223

23-
def test_id_max_value_and_conversion():
24-
max_id = 9223372036854775807
25-
min_id = -9223372036854775808
26-
max_hex = "7fffffffffffffff"
27-
min_hex = "8000000000000000"
28-
29-
assert_equals(max_hex, instana.util.id_to_header(max_id))
30-
assert_equals(min_hex, instana.util.id_to_header(min_id))
31-
32-
assert_equals(max_id, instana.util.header_to_id(max_hex))
33-
assert_equals(min_id, instana.util.header_to_id(min_hex))
34-
35-
36-
def test_id_conversion_back_and_forth():
37-
# id --> header --> id
38-
original_id = instana.util.generate_id()
39-
header_id = instana.util.id_to_header(original_id)
40-
converted_back_id = instana.util.header_to_id(header_id)
41-
assert original_id == converted_back_id
42-
43-
# header --> id --> header
44-
original_header_id = "c025ee93b1aeda7b"
45-
id = instana.util.header_to_id(original_header_id)
46-
converted_back_header_id = instana.util.id_to_header(id)
47-
assert_equals(original_header_id, converted_back_header_id)
48-
49-
# Test a random value
50-
id = -7815363404733516491
51-
header = "938a406416457535"
52-
53-
result = instana.util.header_to_id(header)
54-
assert_equals(id, result)
55-
56-
result = instana.util.id_to_header(id)
57-
assert_equals(header, result)
58-
59-
60-
def test_that_leading_zeros_handled_correctly():
61-
header = instana.util.id_to_header(16)
62-
assert_equals("10", header)
63-
64-
id = instana.util.header_to_id("10")
65-
assert_equals(16, id)
66-
67-
id = instana.util.header_to_id("0000000000000010")
68-
assert_equals(16, id)
69-
70-
id = instana.util.header_to_id("88b6c735206ca42")
71-
assert_equals(615705016619420226, id)
72-
73-
id = instana.util.header_to_id("088b6c735206ca42")
74-
assert_equals(615705016619420226, id)
75-
76-
77-
def test_id_to_header_conversion():
78-
# Test passing a standard Integer ID
79-
original_id = instana.util.generate_id()
80-
converted_id = instana.util.id_to_header(original_id)
81-
82-
# Assert that it is a string and there are no non-hex characters
83-
assert isinstance(converted_id, string_types)
84-
assert all(c in string.hexdigits for c in converted_id)
85-
86-
# Test passing a standard Integer ID as a String
87-
original_id = instana.util.generate_id()
88-
converted_id = instana.util.id_to_header(original_id)
89-
90-
# Assert that it is a string and there are no non-hex characters
91-
assert isinstance(converted_id, string_types)
92-
assert all(c in string.hexdigits for c in converted_id)
93-
94-
95-
def test_id_to_header_conversion_with_bogus_id():
96-
# Test passing an empty String
97-
converted_id = instana.util.id_to_header('')
98-
99-
# Assert that it is a string and there are no non-hex characters
100-
assert isinstance(converted_id, string_types)
101-
assert converted_id == instana.util.BAD_ID_HEADER
102-
103-
# Test passing a nil
104-
converted_id = instana.util.id_to_header(None)
105-
106-
# Assert that it is a string and there are no non-hex characters
107-
assert isinstance(converted_id, string_types)
108-
assert converted_id == instana.util.BAD_ID_HEADER
109-
110-
# Test passing an Array
111-
converted_id = instana.util.id_to_header([])
112-
113-
# Assert that it is a string and there are no non-hex characters
114-
assert isinstance(converted_id, string_types)
115-
assert converted_id == instana.util.BAD_ID_HEADER
116-
117-
118-
def test_header_to_id_conversion():
24+
def test_various_header_to_id_conversion():
11925
# Get a hex string to test against & convert
120-
header_id = instana.util.id_to_header(instana.util.generate_id)
26+
header_id = instana.util.generate_id()
12127
converted_id = instana.util.header_to_id(header_id)
28+
assert_equals(header_id, converted_id)
12229

123-
# Assert that it is an Integer
124-
assert isinstance(converted_id, int)
30+
# Hex value - result should be left padded
31+
result = instana.util.header_to_id('abcdef')
32+
assert_equals('0000000000abcdef', result)
33+
34+
# Hex value
35+
result = instana.util.header_to_id('0123456789abcdef')
36+
assert_equals('0123456789abcdef', result)
37+
38+
# Very long incoming header should just return the rightmost 16 bytes
39+
result = instana.util.header_to_id('0x0123456789abcdef0123456789abcdef')
40+
assert_equals('0123456789abcdef', result)
12541

12642

12743
def test_header_to_id_conversion_with_bogus_header():
12844
# Bogus nil arg
12945
bogus_result = instana.util.header_to_id(None)
130-
assert_equals(instana.util.BAD_ID_LONG, bogus_result)
46+
assert_equals(instana.util.BAD_ID, bogus_result)
13147

13248
# Bogus Integer arg
13349
bogus_result = instana.util.header_to_id(1234)
134-
assert_equals(instana.util.BAD_ID_LONG, bogus_result)
50+
assert_equals(instana.util.BAD_ID, bogus_result)
13551

13652
# Bogus Array arg
13753
bogus_result = instana.util.header_to_id([1234])
138-
assert_equals(instana.util.BAD_ID_LONG, bogus_result)
54+
assert_equals(instana.util.BAD_ID, bogus_result)
55+
56+
# Bogus Hex Values in String
57+
bogus_result = instana.util.header_to_id('0xZZZZZZ')
58+
assert_equals(instana.util.BAD_ID, bogus_result)
59+
60+
bogus_result = instana.util.header_to_id('ZZZZZZ')
61+
assert_equals(instana.util.BAD_ID, bogus_result)

tests/test_ot_propagators.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ def test_inject():
3030
ot.tracer.inject(span.context, ot.Format.HTTP_HEADERS, carrier)
3131

3232
assert 'X-Instana-T' in carrier
33-
assert_equals(carrier['X-Instana-T'], util.id_to_header(span.context.trace_id))
33+
assert_equals(carrier['X-Instana-T'], span.context.trace_id)
3434
assert 'X-Instana-S' in carrier
35-
assert_equals(carrier['X-Instana-S'], util.id_to_header(span.context.span_id))
35+
assert_equals(carrier['X-Instana-S'], span.context.span_id)
3636
assert 'X-Instana-L' in carrier
3737
assert_equals(carrier['X-Instana-L'], "1")
3838

@@ -45,8 +45,8 @@ def test_basic_extract():
4545
ctx = ot.tracer.extract(ot.Format.HTTP_HEADERS, carrier)
4646

4747
assert type(ctx) is basictracer.context.SpanContext
48-
assert_equals(1, ctx.trace_id)
49-
assert_equals(1, ctx.span_id)
48+
assert_equals('0000000000000001', ctx.trace_id)
49+
assert_equals('0000000000000001', ctx.span_id)
5050

5151

5252
def test_mixed_case_extract():
@@ -57,8 +57,8 @@ def test_mixed_case_extract():
5757
ctx = ot.tracer.extract(ot.Format.HTTP_HEADERS, carrier)
5858

5959
assert type(ctx) is basictracer.context.SpanContext
60-
assert_equals(1, ctx.trace_id)
61-
assert_equals(1, ctx.span_id)
60+
assert_equals('0000000000000001', ctx.trace_id)
61+
assert_equals('0000000000000001', ctx.span_id)
6262

6363

6464
def test_no_context_extract():
@@ -69,3 +69,17 @@ def test_no_context_extract():
6969
ctx = ot.tracer.extract(ot.Format.HTTP_HEADERS, carrier)
7070

7171
assert ctx is None
72+
73+
74+
def test_128bit_headers():
75+
opts = options.Options()
76+
ot.tracer = InstanaTracer(opts)
77+
78+
carrier = {'X-Instana-T': '0000000000000000b0789916ff8f319f',
79+
'X-Instana-S': '0000000000000000b0789916ff8f319f', 'X-Instana-L': '1'}
80+
ctx = ot.tracer.extract(ot.Format.HTTP_HEADERS, carrier)
81+
82+
assert type(ctx) is basictracer.context.SpanContext
83+
assert_equals('b0789916ff8f319f', ctx.trace_id)
84+
assert_equals('b0789916ff8f319f', ctx.span_id)
85+

0 commit comments

Comments
 (0)