Skip to content

Commit 10449bf

Browse files
committed
Add support for Oracle Cloud Infrastructure tracing solution by providing a tracing context
Updated test-requirements dependencies
1 parent 874254b commit 10449bf

4 files changed

Lines changed: 365 additions & 9 deletions

File tree

fdk/constants.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@
3030
FN_ID = "FN_FN_ID"
3131
FN_LOGFRAME_NAME = "FN_LOGFRAME_NAME"
3232
FN_LOGFRAME_HDR = "FN_LOGFRAME_HDR"
33+
FN_APP_NAME = "FN_APP_NAME"
34+
FN_NAME = "FN_FN_NAME"
35+
OCI_TRACE_COLLECTOR_URL = "OCI_TRACE_COLLECTOR_URL"
36+
OCI_TRACING_ENABLED = "OCI_TRACING_ENABLED"
37+
3338

3439
# headers are lower case TODO(denis): why?
3540
FN_INTENT = "fn-intent"
@@ -42,6 +47,11 @@
4247
FN_HTTP_METHOD = "fn-http-method"
4348
CONTENT_TYPE = "content-type"
4449
CONTENT_LENGTH = "content-length"
50+
X_B3_TRACEID = "x-b3-traceid"
51+
X_B3_SPANID = "x-b3-spanid"
52+
X_B3_PARENTSPANID = "x-b3-parentspanid"
53+
X_B3_SAMPLED = "x-b3-sampled"
54+
X_B3_FLAGS = "x-b3-flags"
4555

4656
FN_ENFORCED_RESPONSE_CODES = [200, 502, 504]
4757
FN_DEFAULT_RESPONSE_CODE = 200

fdk/context.py

Lines changed: 183 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,32 @@
1717
import datetime as dt
1818
import io
1919
import os
20-
20+
import random
2121
from fdk import constants
2222
from fdk import headers as hs
2323
from fdk import log
24+
from collections import namedtuple
2425

2526

2627
class InvokeContext(object):
2728

28-
def __init__(self, app_id, fn_id, call_id,
29+
def __init__(self, app_id, app_name, fn_id, fn_name, call_id,
2930
content_type="application/octet-stream",
3031
deadline=None, config=None,
3132
headers=None, request_url=None,
32-
method="POST", fn_format=None):
33+
method="POST", fn_format=None,
34+
tracing_context=None):
3335
"""
3436
Request context here to be a placeholder
3537
for request-specific attributes
3638
:param app_id: Fn App ID
3739
:type app_id: str
40+
:param app_name: Fn App name
41+
:type app_name: str
3842
:param fn_id: Fn App Fn ID
3943
:type fn_id: str
44+
:param fn_name: Fn name
45+
:type fn_name: str
4046
:param call_id: Fn call ID
4147
:type call_id: str
4248
:param content_type: request content type
@@ -53,6 +59,8 @@ def __init__(self, app_id, fn_id, call_id,
5359
:type method: str
5460
:param fn_format: function format
5561
:type fn_format: str
62+
:param tracing_context: tracing context
63+
:type tracing_context: TracingContext
5664
"""
5765
self.__app_id = app_id
5866
self.__fn_id = fn_id
@@ -66,6 +74,9 @@ def __init__(self, app_id, fn_id, call_id,
6674
self._method = method
6775
self.__response_headers = {}
6876
self.__fn_format = fn_format
77+
self.__app_name = app_name
78+
self.__fn_name = fn_name
79+
self.__tracing_context = tracing_context if tracing_context else None
6980

7081
log.log("request headers. gateway: {0} {1}"
7182
.format(self.__is_gateway(), headers))
@@ -77,9 +88,15 @@ def __init__(self, app_id, fn_id, call_id,
7788
def AppID(self):
7889
return self.__app_id
7990

91+
def AppName(self):
92+
return self.__app_name
93+
8094
def FnID(self):
8195
return self.__fn_id
8296

97+
def FnName(self):
98+
return self.__fn_name
99+
83100
def CallID(self):
84101
return self.__call_id
85102

@@ -95,6 +112,9 @@ def HTTPHeaders(self):
95112
def Format(self):
96113
return self.__fn_format
97114

115+
def TracingContext(self):
116+
return self.__tracing_context
117+
98118
def Deadline(self):
99119
if self.__deadline is None:
100120
now = dt.datetime.now(dt.timezone.utc).astimezone()
@@ -125,6 +145,114 @@ def __is_gateway(self):
125145
== constants.INTENT_HTTP_REQUEST)
126146

127147

148+
class TracingContext(object):
149+
150+
def __init__(self, is_tracing_enabled, trace_collector_url,
151+
trace_id, span_id, parent_span_id,
152+
is_sampled, flags):
153+
"""
154+
Tracing context here to be a placeholder
155+
for tracing-specific attributes
156+
:param is_tracing_enabled: tracing enabled flag
157+
:type is_tracing_enabled: bool
158+
:param trace_collector_url: APM Trace Collector Endpoint URL
159+
:type trace_collector_url: str
160+
:param trace_id: Trace ID
161+
:type trace_id: str
162+
:param span_id: Span ID
163+
:type span_id: str
164+
:param parent_span_id: Parent Span ID
165+
:type parent_span_id: str
166+
:param is_sampled: Boolean for emmitting spans
167+
:type is_sampled: int (0 or 1)
168+
:param flags: Debug flags
169+
:type flags: int (0 or 1)
170+
"""
171+
self.__is_tracing_enabled = is_tracing_enabled
172+
self.__trace_collector_url = trace_collector_url
173+
self.__trace_id = trace_id
174+
self.__span_id = span_id
175+
self.__parent_span_id = parent_span_id
176+
self.__is_sampled = is_sampled
177+
self.__flags = flags
178+
self.__app_name = os.environ.get(constants.FN_APP_NAME)
179+
self.__app_id = os.environ.get(constants.FN_APP_ID)
180+
self.__fn_name = os.environ.get(constants.FN_NAME)
181+
self.__fn_id = os.environ.get(constants.FN_ID)
182+
183+
def is_tracing_enabled(self):
184+
return self.__is_tracing_enabled
185+
186+
def trace_collector_url(self):
187+
return self.__trace_collector_url
188+
189+
def trace_id(self):
190+
return self.__trace_id
191+
192+
def span_id(self):
193+
return self.__span_id
194+
195+
def parent_span_id(self):
196+
return self.__parent_span_id
197+
198+
def is_sampled(self):
199+
return bool(self.__is_sampled)
200+
201+
def flags(self):
202+
return self.__flags
203+
204+
# this is a helper method specific for py_zipkin
205+
def zipkin_attrs(self):
206+
ZipkinAttrs = namedtuple(
207+
"ZipkinAttrs",
208+
"trace_id, span_id, parent_span_id, is_sampled, flags"
209+
)
210+
211+
trace_id = self.__trace_id
212+
span_id = self.__span_id
213+
parent_span_id = self.__parent_span_id
214+
is_sampled = bool(self.__is_sampled)
215+
trace_flags = self.__flags
216+
217+
# As the fnLb sends the parent_span_id as the span_id
218+
# assign the parent span id as the span id.
219+
if parent_span_id is None and span_id is not None:
220+
parent_span_id = span_id
221+
span_id = generate_id()
222+
223+
zipkin_attrs = ZipkinAttrs(
224+
trace_id,
225+
span_id,
226+
parent_span_id,
227+
is_sampled,
228+
trace_flags
229+
)
230+
return zipkin_attrs
231+
232+
def service_name(self, override=None):
233+
# in case of missing app and function name env variables
234+
service_name = (
235+
override
236+
if override is not None
237+
else str(self.__app_name) + "::" + str(self.__fn_name)
238+
)
239+
return service_name.lower()
240+
241+
def annotations(self):
242+
annotations = {
243+
"generatedBy": "faas",
244+
"appName": self.__app_name,
245+
"appID": self.__app_id,
246+
"fnName": self.__fn_name,
247+
"fnID": self.__fn_id,
248+
}
249+
return annotations
250+
251+
252+
def generate_id():
253+
return "{:016x}".format(random.getrandbits(64))
254+
255+
128256
def context_from_format(format_def: str, **kwargs) -> (
129257
InvokeContext, io.BytesIO):
130258
"""
@@ -138,27 +266,76 @@ def context_from_format(format_def: str, **kwargs) -> (
138266

139267
app_id = os.environ.get(constants.FN_APP_ID)
140268
fn_id = os.environ.get(constants.FN_ID)
269+
app_name = os.environ.get(constants.FN_APP_NAME)
270+
fn_name = os.environ.get(constants.FN_NAME)
271+
# the tracing enabled env variable is passed as a "0" or "1" string
272+
# and therefore needs to be converted appropriately.
273+
is_tracing_enabled = os.environ.get(constants.OCI_TRACING_ENABLED)
274+
is_tracing_enabled = (
275+
bool(int(is_tracing_enabled))
276+
if is_tracing_enabled is not None
277+
else False
278+
)
279+
trace_collector_url = os.environ.get(constants.OCI_TRACE_COLLECTOR_URL)
141280

142281
if format_def == constants.HTTPSTREAM:
143282
data = kwargs.get("data")
144283
headers = kwargs.get("headers")
145284

285+
# zipkin tracing http headers
286+
trace_id = span_id = parent_span_id = is_sampled = trace_flags = None
287+
tracing_context = None
288+
if is_tracing_enabled:
289+
# we generate the trace_id if tracing is enabled
290+
# but the traceId zipkin header is missing.
291+
trace_id = headers.get(constants.X_B3_TRACEID)
292+
trace_id = generate_id() if trace_id is None else trace_id
293+
294+
span_id = headers.get(constants.X_B3_SPANID)
295+
parent_span_id = headers.get(constants.X_B3_PARENTSPANID)
296+
297+
# span_id is also generated if the zipkin header is missing.
298+
span_id = generate_id() if span_id is None else span_id
299+
300+
# is_sampled should be a boolean in the form of a "0/1" but
301+
# legacy samples have them as "False/True"
302+
is_sampled = headers.get(constants.X_B3_SAMPLED)
303+
is_sampled = int(is_sampled) if is_sampled is not None else 1
304+
305+
# not currently used but is defined by the zipkin headers standard
306+
trace_flags = headers.get(constants.X_B3_FLAGS)
307+
308+
# tracing context will be an empty object
309+
# if tracing is not enabled or the flag is missing.
310+
# this prevents the customer code from failing if they decide to
311+
# disable tracing. An empty tracing context will not
312+
# emit spans due to is_sampled being None.
313+
tracing_context = TracingContext(
314+
is_tracing_enabled,
315+
trace_collector_url,
316+
trace_id,
317+
span_id,
318+
parent_span_id,
319+
is_sampled,
320+
trace_flags
321+
)
322+
146323
method = headers.get(constants.FN_HTTP_METHOD)
147-
request_url = headers.get(
148-
constants.FN_HTTP_REQUEST_URL)
324+
request_url = headers.get(constants.FN_HTTP_REQUEST_URL)
149325
deadline = headers.get(constants.FN_DEADLINE)
150326
call_id = headers.get(constants.FN_CALL_ID)
151327
content_type = headers.get(constants.CONTENT_TYPE)
152328

153329
ctx = InvokeContext(
154-
app_id, fn_id, call_id,
330+
app_id, app_name, fn_id, fn_name, call_id,
155331
content_type=content_type,
156332
deadline=deadline,
157333
config=os.environ,
158334
headers=headers,
159335
method=method,
160336
request_url=request_url,
161337
fn_format=constants.HTTPSTREAM,
338+
tracing_context=tracing_context,
162339
)
163340

164341
return ctx, data

0 commit comments

Comments
 (0)