Skip to content

Commit 9d74918

Browse files
Andrew Xueaabmassocelotl
authored
add cloud trace propagator (#819)
Adding initial cloud trace propagator Co-authored-by: Aaron Abbott <[email protected]> Co-authored-by: Diego Hurtado <[email protected]>
1 parent a284367 commit 9d74918

File tree

4 files changed

+346
-13
lines changed

4 files changed

+346
-13
lines changed

ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@
5454
from opentelemetry.sdk.trace import Event
5555
from opentelemetry.sdk.trace.export import Span, SpanExporter, SpanExportResult
5656
from opentelemetry.sdk.util import BoundedDict
57+
from opentelemetry.trace.span import (
58+
get_hexadecimal_span_id,
59+
get_hexadecimal_trace_id,
60+
)
5761
from opentelemetry.util import types
5862

5963
logger = logging.getLogger(__name__)
@@ -123,15 +127,15 @@ def _translate_to_cloud_trace(
123127

124128
for span in spans:
125129
ctx = span.get_context()
126-
trace_id = _get_hexadecimal_trace_id(ctx.trace_id)
127-
span_id = _get_hexadecimal_span_id(ctx.span_id)
130+
trace_id = get_hexadecimal_trace_id(ctx.trace_id)
131+
span_id = get_hexadecimal_span_id(ctx.span_id)
128132
span_name = "projects/{}/traces/{}/spans/{}".format(
129133
self.project_id, trace_id, span_id
130134
)
131135

132136
parent_id = None
133137
if span.parent:
134-
parent_id = _get_hexadecimal_span_id(span.parent.span_id)
138+
parent_id = get_hexadecimal_span_id(span.parent.span_id)
135139

136140
start_time = _get_time_from_ns(span.start_time)
137141
end_time = _get_time_from_ns(span.end_time)
@@ -169,14 +173,6 @@ def shutdown(self):
169173
pass
170174

171175

172-
def _get_hexadecimal_trace_id(trace_id: int) -> str:
173-
return "{:032x}".format(trace_id)
174-
175-
176-
def _get_hexadecimal_span_id(span_id: int) -> str:
177-
return "{:016x}".format(span_id)
178-
179-
180176
def _get_time_from_ns(nanoseconds: int) -> Dict:
181177
"""Given epoch nanoseconds, split into epoch milliseconds and remaining
182178
nanoseconds"""
@@ -234,8 +230,8 @@ def _extract_links(links: Sequence[trace_api.Link]) -> ProtoSpan.Links:
234230
"Link has more then %s attributes, some will be truncated",
235231
MAX_LINK_ATTRS,
236232
)
237-
trace_id = _get_hexadecimal_trace_id(link.context.trace_id)
238-
span_id = _get_hexadecimal_span_id(link.context.span_id)
233+
trace_id = get_hexadecimal_trace_id(link.context.trace_id)
234+
span_id = get_hexadecimal_span_id(link.context.span_id)
239235
extracted_links.append(
240236
{
241237
"trace_id": trace_id,
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
16+
import re
17+
import typing
18+
19+
import opentelemetry.trace as trace
20+
from opentelemetry.context.context import Context
21+
from opentelemetry.trace.propagation import httptextformat
22+
from opentelemetry.trace.span import (
23+
SpanContext,
24+
TraceFlags,
25+
get_hexadecimal_trace_id,
26+
)
27+
28+
_TRACE_CONTEXT_HEADER_NAME = "X-Cloud-Trace-Context"
29+
_TRACE_CONTEXT_HEADER_FORMAT = r"(?P<trace_id>[0-9a-f]{32})\/(?P<span_id>[\d]{1,20});o=(?P<trace_flags>\d+)"
30+
_TRACE_CONTEXT_HEADER_RE = re.compile(_TRACE_CONTEXT_HEADER_FORMAT)
31+
32+
33+
class CloudTraceFormatPropagator(httptextformat.HTTPTextFormat):
34+
"""This class is for injecting into a carrier the SpanContext in Google
35+
Cloud format, or extracting the SpanContext from a carrier using Google
36+
Cloud format.
37+
"""
38+
39+
def extract(
40+
self,
41+
get_from_carrier: httptextformat.Getter[
42+
httptextformat.HTTPTextFormatT
43+
],
44+
carrier: httptextformat.HTTPTextFormatT,
45+
context: typing.Optional[Context] = None,
46+
) -> Context:
47+
header = get_from_carrier(carrier, _TRACE_CONTEXT_HEADER_NAME)
48+
49+
if not header:
50+
return trace.set_span_in_context(trace.INVALID_SPAN, context)
51+
52+
match = re.fullmatch(_TRACE_CONTEXT_HEADER_RE, header[0])
53+
if match is None:
54+
return trace.set_span_in_context(trace.INVALID_SPAN, context)
55+
56+
trace_id = match.group("trace_id")
57+
span_id = match.group("span_id")
58+
trace_options = match.group("trace_flags")
59+
60+
if trace_id == "0" * 32 or int(span_id) == 0:
61+
return trace.set_span_in_context(trace.INVALID_SPAN, context)
62+
63+
span_context = SpanContext(
64+
trace_id=int(trace_id, 16),
65+
span_id=int(span_id),
66+
is_remote=True,
67+
trace_flags=TraceFlags(trace_options),
68+
)
69+
return trace.set_span_in_context(
70+
trace.DefaultSpan(span_context), context
71+
)
72+
73+
def inject(
74+
self,
75+
set_in_carrier: httptextformat.Setter[httptextformat.HTTPTextFormatT],
76+
carrier: httptextformat.HTTPTextFormatT,
77+
context: typing.Optional[Context] = None,
78+
) -> None:
79+
span = trace.get_current_span(context)
80+
if span is None:
81+
return
82+
span_context = span.get_context()
83+
if span_context == trace.INVALID_SPAN_CONTEXT:
84+
return
85+
86+
header = "{}/{};o={}".format(
87+
get_hexadecimal_trace_id(span_context.trace_id),
88+
span_context.span_id,
89+
int(span_context.trace_flags.sampled),
90+
)
91+
set_in_carrier(carrier, _TRACE_CONTEXT_HEADER_NAME, header)
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
import typing
16+
import unittest
17+
18+
import opentelemetry.trace as trace
19+
from opentelemetry.context import get_current
20+
from opentelemetry.exporter.cloud_trace.cloud_trace_propagator import (
21+
_TRACE_CONTEXT_HEADER_NAME,
22+
CloudTraceFormatPropagator,
23+
)
24+
from opentelemetry.trace.span import (
25+
INVALID_SPAN_ID,
26+
INVALID_TRACE_ID,
27+
SpanContext,
28+
TraceFlags,
29+
get_hexadecimal_trace_id,
30+
)
31+
32+
33+
def get_dict_value(dict_object: typing.Dict[str, str], key: str) -> str:
34+
return dict_object.get(key, "")
35+
36+
37+
class TestCloudTraceFormatPropagator(unittest.TestCase):
38+
def setUp(self):
39+
self.propagator = CloudTraceFormatPropagator()
40+
self.valid_trace_id = 281017822499060589596062859815111849546
41+
self.valid_span_id = 17725314949316355921
42+
self.too_long_id = 111111111111111111111111111111111111111111111
43+
44+
def _extract(self, header_value):
45+
"""Test helper"""
46+
header = {_TRACE_CONTEXT_HEADER_NAME: [header_value]}
47+
new_context = self.propagator.extract(get_dict_value, header)
48+
return trace.get_current_span(new_context).get_context()
49+
50+
def _inject(self, span=None):
51+
"""Test helper"""
52+
ctx = get_current()
53+
if span is not None:
54+
ctx = trace.set_span_in_context(span, ctx)
55+
output = {}
56+
self.propagator.inject(dict.__setitem__, output, context=ctx)
57+
return output.get(_TRACE_CONTEXT_HEADER_NAME)
58+
59+
def test_no_context_header(self):
60+
header = {}
61+
new_context = self.propagator.extract(get_dict_value, header)
62+
self.assertEqual(
63+
trace.get_current_span(new_context).get_context(),
64+
trace.INVALID_SPAN.get_context(),
65+
)
66+
67+
def test_empty_context_header(self):
68+
header = ""
69+
self.assertEqual(
70+
self._extract(header), trace.INVALID_SPAN.get_context()
71+
)
72+
73+
def test_valid_header(self):
74+
header = "{}/{};o=1".format(
75+
get_hexadecimal_trace_id(self.valid_trace_id), self.valid_span_id
76+
)
77+
new_span_context = self._extract(header)
78+
self.assertEqual(new_span_context.trace_id, self.valid_trace_id)
79+
self.assertEqual(new_span_context.span_id, self.valid_span_id)
80+
self.assertEqual(new_span_context.trace_flags, TraceFlags(1))
81+
self.assertTrue(new_span_context.is_remote)
82+
83+
header = "{}/{};o=10".format(
84+
get_hexadecimal_trace_id(self.valid_trace_id), self.valid_span_id
85+
)
86+
new_span_context = self._extract(header)
87+
self.assertEqual(new_span_context.trace_id, self.valid_trace_id)
88+
self.assertEqual(new_span_context.span_id, self.valid_span_id)
89+
self.assertEqual(new_span_context.trace_flags, TraceFlags(10))
90+
self.assertTrue(new_span_context.is_remote)
91+
92+
header = "{}/{};o=0".format(
93+
get_hexadecimal_trace_id(self.valid_trace_id), self.valid_span_id
94+
)
95+
new_span_context = self._extract(header)
96+
self.assertEqual(new_span_context.trace_id, self.valid_trace_id)
97+
self.assertEqual(new_span_context.span_id, self.valid_span_id)
98+
self.assertEqual(new_span_context.trace_flags, TraceFlags(0))
99+
self.assertTrue(new_span_context.is_remote)
100+
101+
header = "{}/{};o=0".format(
102+
get_hexadecimal_trace_id(self.valid_trace_id), 345
103+
)
104+
new_span_context = self._extract(header)
105+
self.assertEqual(new_span_context.trace_id, self.valid_trace_id)
106+
self.assertEqual(new_span_context.span_id, 345)
107+
self.assertEqual(new_span_context.trace_flags, TraceFlags(0))
108+
self.assertTrue(new_span_context.is_remote)
109+
110+
def test_invalid_header_format(self):
111+
header = "invalid_header"
112+
self.assertEqual(
113+
self._extract(header), trace.INVALID_SPAN.get_context()
114+
)
115+
116+
header = "{}/{};o=".format(
117+
get_hexadecimal_trace_id(self.valid_trace_id), self.valid_span_id
118+
)
119+
self.assertEqual(
120+
self._extract(header), trace.INVALID_SPAN.get_context()
121+
)
122+
123+
header = "extra_chars/{}/{};o=1".format(
124+
get_hexadecimal_trace_id(self.valid_trace_id), self.valid_span_id
125+
)
126+
self.assertEqual(
127+
self._extract(header), trace.INVALID_SPAN.get_context()
128+
)
129+
130+
header = "{}/{}extra_chars;o=1".format(
131+
get_hexadecimal_trace_id(self.valid_trace_id), self.valid_span_id
132+
)
133+
self.assertEqual(
134+
self._extract(header), trace.INVALID_SPAN.get_context()
135+
)
136+
137+
header = "{}/{};o=1extra_chars".format(
138+
get_hexadecimal_trace_id(self.valid_trace_id), self.valid_span_id
139+
)
140+
self.assertEqual(
141+
self._extract(header), trace.INVALID_SPAN.get_context()
142+
)
143+
144+
header = "{}/;o=1".format(
145+
get_hexadecimal_trace_id(self.valid_trace_id)
146+
)
147+
self.assertEqual(
148+
self._extract(header), trace.INVALID_SPAN.get_context()
149+
)
150+
151+
header = "/{};o=1".format(self.valid_span_id)
152+
self.assertEqual(
153+
self._extract(header), trace.INVALID_SPAN.get_context()
154+
)
155+
156+
header = "{}/{};o={}".format("123", "34", "4")
157+
self.assertEqual(
158+
self._extract(header), trace.INVALID_SPAN.get_context()
159+
)
160+
161+
def test_invalid_trace_id(self):
162+
header = "{}/{};o={}".format(INVALID_TRACE_ID, self.valid_span_id, 1)
163+
self.assertEqual(
164+
self._extract(header), trace.INVALID_SPAN.get_context()
165+
)
166+
header = "{}/{};o={}".format("0" * 32, self.valid_span_id, 1)
167+
self.assertEqual(
168+
self._extract(header), trace.INVALID_SPAN.get_context()
169+
)
170+
171+
header = "0/{};o={}".format(self.valid_span_id, 1)
172+
self.assertEqual(
173+
self._extract(header), trace.INVALID_SPAN.get_context()
174+
)
175+
176+
header = "234/{};o={}".format(self.valid_span_id, 1)
177+
self.assertEqual(
178+
self._extract(header), trace.INVALID_SPAN.get_context()
179+
)
180+
181+
header = "{}/{};o={}".format(self.too_long_id, self.valid_span_id, 1)
182+
self.assertEqual(
183+
self._extract(header), trace.INVALID_SPAN.get_context()
184+
)
185+
186+
def test_invalid_span_id(self):
187+
header = "{}/{};o={}".format(
188+
get_hexadecimal_trace_id(self.valid_trace_id), INVALID_SPAN_ID, 1
189+
)
190+
self.assertEqual(
191+
self._extract(header), trace.INVALID_SPAN.get_context()
192+
)
193+
194+
header = "{}/{};o={}".format(
195+
get_hexadecimal_trace_id(self.valid_trace_id), "0" * 16, 1
196+
)
197+
self.assertEqual(
198+
self._extract(header), trace.INVALID_SPAN.get_context()
199+
)
200+
201+
header = "{}/{};o={}".format(
202+
get_hexadecimal_trace_id(self.valid_trace_id), "0", 1
203+
)
204+
self.assertEqual(
205+
self._extract(header), trace.INVALID_SPAN.get_context()
206+
)
207+
208+
header = "{}/{};o={}".format(
209+
get_hexadecimal_trace_id(self.valid_trace_id), self.too_long_id, 1
210+
)
211+
self.assertEqual(
212+
self._extract(header), trace.INVALID_SPAN.get_context()
213+
)
214+
215+
def test_inject_with_no_context(self):
216+
output = self._inject()
217+
self.assertIsNone(output)
218+
219+
def test_inject_with_invalid_context(self):
220+
output = self._inject(trace.INVALID_SPAN)
221+
self.assertIsNone(output)
222+
223+
def test_inject_with_valid_context(self):
224+
span_context = SpanContext(
225+
trace_id=self.valid_trace_id,
226+
span_id=self.valid_span_id,
227+
is_remote=True,
228+
trace_flags=TraceFlags(1),
229+
)
230+
output = self._inject(trace.DefaultSpan(span_context))
231+
self.assertEqual(
232+
output,
233+
"{}/{};o={}".format(
234+
get_hexadecimal_trace_id(self.valid_trace_id),
235+
self.valid_span_id,
236+
1,
237+
),
238+
)

opentelemetry-api/src/opentelemetry/trace/span.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,11 @@ def format_trace_id(trace_id: int) -> str:
279279

280280
def format_span_id(span_id: int) -> str:
281281
return "0x{:016x}".format(span_id)
282+
283+
284+
def get_hexadecimal_trace_id(trace_id: int) -> str:
285+
return "{:032x}".format(trace_id)
286+
287+
288+
def get_hexadecimal_span_id(span_id: int) -> str:
289+
return "{:016x}".format(span_id)

0 commit comments

Comments
 (0)