Skip to content

Commit bf35ccc

Browse files
authored
Add support for Oracle Cloud Infrastructure tracing solution by providing a tracing context #117
OVERVIEW Oracle Functions will soon release a feature providing an integration with the Oracle Application Performance Monitoring (APM) service. This service supports the sending of Zipkin formatted tracing data to a collector endpoint. The service will provide the function code with data which includes: A boolean flag specifying whether the tracing integration is enabled for the function invocation A trace collector URL that can be used to configure Zipkin so that tracing data is sent there The "B3" formatted headers specifying the originating trace ID and span IDs (as specified by Zipkin) This information is accessible by the function code in a Tracing Context provided by the FDK. This change has no effect on existing functions, except that the keys "OCI_TRACING_ENABLED" and "OCI_TRACE_COLLECTOR_URL" are now reserved for this tracing integration and cannot be used for function configuration. FDK SPECIFIC CHANGES This change adds an additional tracing context to the invoke context. This can be accessed by users to configure tracing in their functions using the zipkin libraries of their choice (py_zipkin seems to be the most popular). The tracing context contains methods to access the extracted zipkin B3 headers as well as the trace collector URL, tracing enabled flag and a few extra helper methods specific to py_zipkin (service name, annotations and zipkin attributes). Additionally, methods to retrieve the app and fn name were added to the invoke context. Changes have been made to constants.py and context.py and unit tests have been added to test_tracing.py This change should not affect existing functions.
2 parents 874254b + 10449bf commit bf35ccc

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)