Skip to content

Commit 38dcb76

Browse files
rhilfersashishagg
authored andcommitted
opentracing updates: ver 2.3.0 (#8)
* opentracing updates: ver 2.3.0 w/ContextVarsScopeManager support. Global Tracer support. protospan boolean tag value bugfix * fix linting error
1 parent e400a43 commit 38dcb76

File tree

7 files changed

+150
-85
lines changed

7 files changed

+150
-85
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@ Further information can be found on [opentracing.io](https://opentracing.io/)
99
## Using this library
1010
See examples in /examples directory. See opentracing [usage](https://github.com/opentracing/opentracing-python/#usage) for additional information.
1111

12+
It is important to consider the architecture of the application. In order for the tracer to manage spans properly, an appropriate `ScopeManager` implementation must be chosen. In most environments, the default `ThreadLocalScopeManager` will work just fine. In asynchronous frameworks, the `ContextVarsScopeManager` is a better choice.
1213

13-
First initialize the tracer at the application level by supplying a service name and recorder
14+
First initialize the tracer at the application level by supplying a service name and span recorder
1415
```python
1516
import opentracing
1617
from haystack import HaystackAgentRecorder
1718
from haystack import HaystackTracer
1819

19-
opentracing.tracer = HaystackTracer("a_service", HaystackAgentRecorder())
20+
tracer = HaystackTracer("a_service", HaystackAgentRecorder())
21+
opentracing.set_global_tracer(tracer)
2022
```
2123

2224
Starting a span can be done as a managed resource using `start_active_span()`

examples/main.py

Lines changed: 94 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,93 @@
11
import opentracing
2-
import time
32
import logging
4-
from opentracing.ext import tags
3+
import time
4+
from opentracing import tags
55
from haystack import HaystackTracer
6-
from haystack import AsyncHttpRecorder
76
from haystack import LoggerRecorder
8-
from haystack.text_propagator import TextPropagator
9-
from haystack.propagator import PropagatorOpts
107

11-
recorder = LoggerRecorder()
12-
logging.basicConfig(level=logging.DEBUG)
138

9+
def setup_tracer():
10+
global recorder
11+
recorder = LoggerRecorder()
1412

15-
def act_as_remote_service(headers):
16-
# remote service would have it"s own tracer
17-
with HaystackTracer("Service-B", recorder,) as tracer:
18-
opentracing.tracer = tracer
13+
# instantiate a haystack tracer for this service and set a common tag which applies to all traces
14+
tracer = HaystackTracer("Service-A",
15+
recorder,
16+
common_tags={"app.version": "1234"})
1917

20-
# ---ability to use custom propagation headers if needed---
21-
# prop_opts = PropagatorOpts("X-Trace-ID", "X-Span-ID", "X-Parent-Span")
22-
# opentracing.tracer.register_propagator(opentracing.Format.HTTP_HEADERS, TextPropagator(prop_opts))
18+
# now set the global tracer, so we can reference it with opentracing.tracer anywhere in our app
19+
opentracing.set_global_tracer(tracer)
2320

21+
22+
def handle_request(request_body):
23+
logging.info(f"handling new request - {request_body}")
24+
25+
# this next line does a few things.. namely, it starts a new scope (which contains the span) to represent
26+
# the scope of this "work". In this case, it should represent the work involved in processing the entire request
27+
with opentracing.tracer.start_active_span("a_controller_method") as parent_scope:
28+
# once within the context of an active span, there are three different ways to assign additional info or
29+
# or attributes to the span
30+
"""
31+
First we'll add some tags to the span
32+
Tags are key:value pairs that enable user-defined annotation of spans in order to query, filter, and
33+
comprehend trace data
34+
Tags have semantic conventions, see https://opentracing.io/specification/conventions/
35+
*tags do NOT propagate to child spans
36+
"""
37+
parent_scope.span.set_tag(tags.HTTP_URL, "http://localhost/mocksvc")
38+
parent_scope.span.set_tag(tags.HTTP_METHOD, "GET")
39+
parent_scope.span.set_tag(tags.SPAN_KIND, "server")
40+
41+
"""
42+
Next we'll add some baggage to the span.
43+
Baggage carries data across process boundaries.. aka it DOES propagate to child spans
44+
"""
45+
parent_scope.span.set_baggage_item("business_id", "1234")
46+
47+
"""
48+
Next lets assume you need to authenticate the client making the request
49+
"""
50+
with opentracing.tracer.start_active_span("authenticate"):
51+
time.sleep(.25) # fake doing some authentication work..
52+
53+
"""
54+
Finally, we'll add a log event to the request level span.
55+
Logs are key:value pairs that are useful for capturing timed log messages and other
56+
debugging or informational output from the application itself. Logs may be useful for
57+
documenting a specific moment or event within the span (in contrast to tags which
58+
should apply to the span regardless of time).
59+
"""
60+
parent_scope.span.log_kv(
61+
{
62+
"some_string_value": "foobar",
63+
"an_int_value": 42,
64+
"a_float_value": 4.2,
65+
"a_bool_value": True,
66+
"an_obj_as_value": {
67+
"ok": "hmm",
68+
"blah": 4324
69+
}
70+
})
71+
72+
try:
73+
"""
74+
Now lets say that as part of processing this request, we need to invoke some downstream service
75+
"""
76+
make_a_downstream_request()
77+
except Exception:
78+
# if that fails, we'll tag the request-scoped span with an error so we have success/fail metrics in haystack
79+
parent_scope.span.set_tag(tags.ERROR, True)
80+
81+
82+
def act_as_remote_service(headers):
83+
# remote service would have it"s own tracer
84+
with HaystackTracer("Service-B", recorder) as tracer:
2485
# simulate network transfer delay
2586
time.sleep(.25)
2687

2788
# now as-if this was executing on the remote service, extract the parent span ctx from headers
28-
upstream_span_ctx = opentracing.tracer.extract(opentracing.Format.HTTP_HEADERS, headers)
29-
with opentracing.tracer.start_active_span("controller_method", child_of=upstream_span_ctx) as parent_scope:
89+
upstream_span_ctx = tracer.extract(opentracing.Format.HTTP_HEADERS, headers)
90+
with tracer.start_active_span("controller_method", child_of=upstream_span_ctx) as parent_scope:
3091
parent_scope.span.set_tag(tags.SPAN_KIND, "server")
3192
# simulate downstream service doing some work before replying
3293
time.sleep(1)
@@ -35,65 +96,38 @@ def act_as_remote_service(headers):
3596
def make_a_downstream_request():
3697
# create a child span representing the downstream request from current span.
3798
# Behind the scenes this uses the scope_manger to access the current active
38-
# span and create a child of it.
99+
# span (which would be our request-scoped span called "a_controller_method" and create a child of it.
39100
with opentracing.tracer.start_active_span("downstream_req") as child_scope:
40-
41101
child_scope.span.set_tag(tags.SPAN_KIND, "client")
42102

43-
# add some baggage (i.e. something that should propagate across
44-
# process boundaries)
45-
child_scope.span.set_baggage_item("baggage-item", "baggage-item-value")
46-
47-
# carrier here represents http headers
48-
carrier = {}
49-
opentracing.tracer.inject(child_scope.span.context, opentracing.Format.HTTP_HEADERS, carrier)
50-
act_as_remote_service(carrier)
103+
# In order for the downstream client to use this trace as a parent, we must propagate the current span context.
104+
# This is done by calling .inject() on the tracer
105+
headers = {}
106+
opentracing.tracer.inject(child_scope.span.context, opentracing.Format.HTTP_HEADERS, headers)
107+
act_as_remote_service(headers)
51108

52109
# process the response from downstream
53110
time.sleep(.15)
54111

55112

56-
def use_http_recorder():
57-
endpoint = "http://<replace_me>"
58-
global recorder
59-
recorder = AsyncHttpRecorder(collector_url=endpoint)
60-
61-
62113
def main():
63114
"""
64-
Represents an application/service
65-
"""
66-
# instantiate a tracer with app version common tag and set it
67-
# to opentracing.tracer property
68-
opentracing.tracer = HaystackTracer("Service-A",
69-
recorder,
70-
common_tags={"app.version": "1234"})
71-
72-
logging.info("mock request received")
73-
with opentracing.tracer.start_active_span("a_controller_method") as parent_scope:
115+
This function represents a "parent" application/service.. i.e. the originating
116+
service of our traces in this example.
74117
75-
# add a tag, tags are part of a span and do not propagate
76-
# (tags have semantic conventions, see https://opentracing.io/specification/conventions/)
77-
parent_scope.span.set_tag(tags.HTTP_URL, "http://localhost/mocksvc")
78-
parent_scope.span.set_tag(tags.HTTP_METHOD, "GET")
79-
parent_scope.span.set_tag(tags.SPAN_KIND, "server")
80-
81-
# doing some work.. validation, processing, etc
82-
time.sleep(.25)
83-
84-
# tag the span with some information about the processing
85-
parent_scope.span.log_kv(
86-
{"string": "foobar", "int": 42, "float": 4.2, "bool": True, "obj": {"ok": "hmm", "blah": 4324}})
118+
In this scenario, we're pretending to be a web server.
119+
"""
87120

88-
make_a_downstream_request()
121+
# at some point during application init, you'll want to instantiate the global tracer
122+
setup_tracer()
89123

90-
# uncomment this line to tag the span with an error
91-
# parent_scope.span.set_tag(tags.ERROR, True)
124+
# here we assume the web framework invokes this method to handle the given request
125+
handle_request("hello world")
92126

93-
logging.info("done in main")
127+
# app shutdown
128+
logging.info("done")
94129

95130

96131
if __name__ == "__main__":
97-
# uncomment line below to send traces to haystack collector using http recorder
98-
# use_http_recorder()
132+
logging.basicConfig(level=logging.DEBUG)
99133
main()

examples/serverless/handler.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import opentracing
33
import os
44
from requests import RequestException
5-
from opentracing.ext import tags
5+
from opentracing import tags
66
from haystack import HaystackTracer
77
from haystack import SyncHttpRecorder
88

@@ -25,8 +25,9 @@
2525
common_tags = {
2626
"svc_ver": os["APP_VERSION"]
2727
}
28-
opentracing.tracer = HaystackTracer("example-service",
29-
recorder, common_tags=common_tags)
28+
tracer = HaystackTracer("example-service",
29+
recorder, common_tags=common_tags)
30+
opentracing.set_global_tracer(tracer)
3031

3132

3233
def invoke_downstream(headers):

haystack/agent_recorder.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,29 @@
88

99

1010
class HaystackAgentRecorder(SpanRecorder):
11+
"""
12+
HaystackAgentRecorder is to be used with the haystack-agent described
13+
here (https://github.com/ExpediaInc/haystack-agent)
14+
"""
1115

1216
def __init__(self, agent_host="haystack-agent", agent_port=35000):
1317
logger.info("Initializing the remote grpc agent recorder, connecting "
1418
f"at {agent_host}:{agent_port}")
1519
channel = grpc.insecure_channel(f"{agent_host}:{agent_port}")
1620
self._stub = spanAgent_pb2_grpc.SpanAgentStub(channel)
1721

18-
def record_span(self, span):
22+
@staticmethod
23+
def process_response(future):
1924
try:
20-
grpc_response = self._stub.dispatch(span_to_proto(span))
25+
grpc_response = future.result()
2126
if grpc_response.code != 0:
2227
logger.error(f"Dispatch failed with {grpc_response.code} due "
2328
f"to {grpc_response.error_message}")
2429
else:
2530
logger.debug("Successfully submitted span to haystack-agent")
2631
except grpc.RpcError:
2732
logger.exception(f"Dispatch failed due to RPC error")
33+
34+
def record_span(self, span):
35+
future = self._stub.dispatch.future(span_to_proto(span))
36+
future.add_done_callback(HaystackAgentRecorder.process_response)

haystack/http_recorder.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ def __init__(self,
4545
@staticmethod
4646
def get_json_payload(span):
4747
json_span = span_to_json(span)
48-
str_span = json.dumps(json_span)
48+
str_span = json.dumps(
49+
json_span,
50+
default=lambda o: f"{o.__class__.__name__} is not serializable"
51+
)
4952
return str_span.encode("utf-8")
5053

5154
@staticmethod
@@ -64,8 +67,11 @@ def post_payload(self, payload):
6467
f"to {e}")
6568

6669
def record_span(self, span):
67-
payload = self.get_json_payload(span) if self._use_json_payload \
68-
else self.get_binary_payload(span)
70+
try:
71+
payload = self.get_json_payload(span) if self._use_json_payload \
72+
else self.get_binary_payload(span)
73+
except TypeError:
74+
logger.exception("failed to convert span")
6975
self.post_payload(payload)
7076

7177

haystack/util.py

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
from .span_pb2 import Span, Tag
22
from .constants import SECONDS_TO_MICRO
3+
from types import TracebackType
34
import numbers
45
import logging
6+
import json
7+
import traceback
58

69
logger = logging.getLogger("haystack")
710

@@ -25,45 +28,54 @@ def logs_as_list(logs):
2528
return log_list
2629

2730

28-
def set_tag_value(tag, value):
29-
if isinstance(value, numbers.Integral):
30-
tag.vLong = value
31-
tag.type = Tag.LONG
32-
elif isinstance(value, str):
31+
def set_proto_tag_value(tag, value):
32+
if isinstance(value, str):
3333
tag.vStr = value
3434
tag.type = Tag.STRING
3535
elif isinstance(value, bool):
3636
tag.vBool = value
3737
tag.type = Tag.BOOL
38+
elif isinstance(value, numbers.Integral):
39+
tag.vLong = value
40+
tag.type = Tag.LONG
3841
elif isinstance(value, float):
3942
tag.vDouble = value
4043
tag.type = Tag.DOUBLE
4144
elif isinstance(value, bytes):
4245
tag.vBytes = value
4346
tag.type = Tag.BINARY
47+
elif isinstance(value, dict):
48+
tag.vStr = json.dumps(value)
49+
tag.type = Tag.STRING
50+
elif isinstance(value, type):
51+
tag.vStr = str(value)
52+
tag.type = Tag.STRING
53+
elif isinstance(value, TracebackType):
54+
tag.vStr = str(traceback.format_tb(value))
55+
tag.type = Tag.STRING
4456
else:
4557
logger.error(f"Dropped tag {tag.key} due to "
4658
f"invalid value type of {type(value)}. "
47-
f"Type must be Int, String, Bool, Float or Bytes")
48-
tag.vStr = ""
59+
f"Type must be Int, String, Bool, Float, Dict or Bytes")
60+
tag.vStr = f"Unserializable object type: {str(type(value))}"
4961
tag.type = Tag.STRING
5062

5163

5264
def add_proto_tags(span_record, tags):
5365
for key, value in tags.items():
5466
tag = span_record.tags.add()
5567
tag.key = key
56-
set_tag_value(tag, value)
68+
set_proto_tag_value(tag, value)
5769

5870

59-
def add_proto_logs(span_record, logs):
60-
for log_data in logs:
71+
def add_proto_logs(span_record, span_logs):
72+
for log_data in span_logs:
6173
log_record = span_record.logs.add()
6274
log_record.timestamp = int(log_data.timestamp * SECONDS_TO_MICRO)
6375
for key, value in log_data.key_values.items():
6476
tag = log_record.fields.add()
6577
tag.key = key
66-
set_tag_value(tag, value)
78+
set_proto_tag_value(tag, value)
6779

6880

6981
def span_to_proto(span):

setup.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
author_email="[email protected]",
1313
long_description=long_description,
1414
long_description_content_type="text/markdown",
15-
install_requires=["opentracing==2.0.0",
15+
install_requires=["opentracing>=2.3.0,<3.0",
1616
"requests>=2.19,<3.0",
17-
"requests-futures==0.9.9,<1.0",
18-
"protobuf>=3.6.0,<4.0",
19-
"grpcio>=1.18.0,<2.0]"],
17+
"requests-futures>=0.9.9,<1.0",
18+
"protobuf>=3.11.2,<4.0",
19+
"grpcio>=1.26.0,<2.0]"],
2020
tests_require=["mock",
2121
"nose",
2222
"pytest",
@@ -26,6 +26,7 @@
2626
"Programming Language :: Python :: 3.5",
2727
"Programming Language :: Python :: 3.6",
2828
"Programming Language :: Python :: 3.7",
29+
"Programming Language :: Python :: 3.8",
2930
"Operating System :: OS Independent",
3031
"License :: OSI Approved :: Apache Software License",
3132
],

0 commit comments

Comments
 (0)