Skip to content

Commit 47c24b5

Browse files
authored
Add policy (#6379)
* add opencensus impl * dont need to check for noop tracer * get rid of span and trace id * fix span instance * added documentation * added documentation and refactor names of a few variables * write test for opencensus wrapper * put opencensus in the dev requirements * initial common * only import the wrapper when necessary * add check for the exporter * rework logic and fix some settings * added initial decorator * some mroe documentation * added decorators * small change * some minor fixes * test patch happening * fix space * share a function * clearer logic for setting span context * better logic * better logic * fix environ variable * test the way opencensus does it * middle of tests * only load if opencensus has already been imported * fix spelling mistake * temp * finish writing tests for common * charles fixes * fix tests * fix test settings * to header should not take a dict * from header should be class method * initial tests * dont create trace and get rid of end_tracer * dont need to save the trace * more little fixes * some intermediatary changes * fix type annotations * rst fix types * add :class:annotations * fix line wrapping * added tests for decorator * rename opencensus wrapper * intermediate changes * use spans the right way * some formatting * some grammar * restructure settings and make tests pass * rename get_parent * fix typings * use protocol and from_headers becomes links * ramifications of opencensus wrapper being a protocol * add tests for link * added async tests * delete the unit test thing * added add_attribute * added add_attribute * added tests for add attributes * add initial policy * remove unused import * added docstrings * minor docstring formatting * fix pylint errors * don't rely on opencensus children to check * use exporter to not rely on parent.children * added documentation and span attributes * added test tracing policy * test should only propagate * made test tracing helper * decrease flakiness of test * simplify get parent * calling a decorator decorator is redundant * middle of writing tests * fix settings * add tests * test propogation also happens * more elegant code * await async stuff * add await for async * should only have to wait a 1/1000 of a second * fix tests spans too short * accidentally deleted setup.cfg * add component * add set_http_attributes * fix span network name * fix http request types * fix more types * bryan fixes * more efficient tests * non flakey tests * make tracing only use my context * test user agent on exception * pylint formatting * delete unused import * fix spelling * pylint
1 parent e197b59 commit 47c24b5

File tree

6 files changed

+308
-65
lines changed

6 files changed

+308
-65
lines changed
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# --------------------------------------------------------------------------
2+
#
3+
# Copyright (c) Microsoft Corporation. All rights reserved.
4+
#
5+
# The MIT License (MIT)
6+
#
7+
# Permission is hereby granted, free of charge, to any person obtaining a copy
8+
# of this software and associated documentation files (the ""Software""), to deal
9+
# in the Software without restriction, including without limitation the rights
10+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
# copies of the Software, and to permit persons to whom the Software is
12+
# furnished to do so, subject to the following conditions:
13+
#
14+
# The above copyright notice and this permission notice shall be included in
15+
# all copies or substantial portions of the Software.
16+
#
17+
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23+
# THE SOFTWARE.
24+
#
25+
# --------------------------------------------------------------------------
26+
"""Traces network calls using the implementation library from the settings."""
27+
28+
from azure.core.pipeline import PipelineRequest, PipelineResponse
29+
from azure.core.tracing.context import tracing_context
30+
from azure.core.tracing.abstract_span import AbstractSpan
31+
from azure.core.tracing.common import set_span_contexts
32+
from azure.core.pipeline.policies import SansIOHTTPPolicy
33+
from azure.core.settings import settings
34+
35+
try:
36+
from urllib.parse import urlparse
37+
except ImportError:
38+
from urlparse import urlparse
39+
40+
try:
41+
from typing import TYPE_CHECKING
42+
except ImportError:
43+
TYPE_CHECKING = False
44+
45+
if TYPE_CHECKING:
46+
from typing import Any, Optional
47+
from azure.core.pipeline.transport import HttpRequest, HttpResponse
48+
49+
50+
class DistributedTracingPolicy(SansIOHTTPPolicy):
51+
"""The policy to create spans for Azure Calls"""
52+
53+
def __init__(self):
54+
# type: (str, str, str) -> None
55+
self.parent_span_dict = {}
56+
self._request_id = "x-ms-request-id"
57+
58+
def set_header(self, request, span):
59+
# type: (PipelineRequest[HttpRequest], Any) -> None
60+
"""
61+
Sets the header information on the span.
62+
"""
63+
headers = span.to_header()
64+
request.http_request.headers.update(headers)
65+
66+
def on_request(self, request, **kwargs):
67+
# type: (PipelineRequest[HttpRequest], Any) -> None
68+
parent_span = tracing_context.current_span.get() # type: AbstractSpan
69+
70+
if parent_span is None:
71+
return
72+
73+
only_propagate = settings.tracing_should_only_propagate()
74+
if only_propagate:
75+
self.set_header(request, parent_span)
76+
return
77+
78+
path = urlparse(request.http_request.url).path
79+
if not path:
80+
path = "/"
81+
child = parent_span.span(name=path)
82+
child.start()
83+
84+
set_span_contexts(child)
85+
self.parent_span_dict[child] = parent_span
86+
self.set_header(request, child)
87+
88+
def end_span(self, request, response=None):
89+
# type: (HttpRequest, Optional[HttpResponse]) -> None
90+
"""Ends the span that is tracing the network and updates its status."""
91+
span = tracing_context.current_span.get() # type: AbstractSpan
92+
only_propagate = settings.tracing_should_only_propagate()
93+
if span and not only_propagate:
94+
span.set_http_attributes(request, response=response)
95+
if response and self._request_id in response.headers:
96+
span.add_attribute(self._request_id, response.headers[self._request_id])
97+
span.finish()
98+
set_span_contexts(self.parent_span_dict[span])
99+
100+
def on_response(self, request, response, **kwargs):
101+
# type: (PipelineRequest[HttpRequest], PipelineResponse[HttpRequest, HttpResponse], Any) -> None
102+
self.end_span(request.http_request, response=response.http_response)
103+
104+
def on_exception(self, _request, **kwargs): # pylint: disable=unused-argument
105+
# type: (PipelineRequest[HttpRequest], Any) -> bool
106+
self.end_span(_request.http_request)

sdk/core/azure-core/azure/core/tracing/abstract_span.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
if TYPE_CHECKING:
1313
from typing import Any, Dict, Optional, Union
1414

15+
from azure.core.pipeline.transport import HttpRequest, HttpResponse
16+
1517
try:
1618
from typing_extensions import Protocol
1719
except ImportError:
@@ -21,7 +23,7 @@
2123
class AbstractSpan(Protocol):
2224
"""Wraps a span from a distributed tracing implementation."""
2325

24-
def __init__(self, span=None, name=None): # pylint: disable=super-init-not-called
26+
def __init__(self, span=None, name=None): # pylint: disable=super-init-not-called
2527
# type: (Optional[Any], Optional[str]) -> None
2628
"""
2729
If a span is given wraps the span. Else a new span is created.
@@ -66,6 +68,18 @@ def add_attribute(self, key, value):
6668
"""
6769
pass
6870

71+
def set_http_attributes(self, request, response=None):
72+
# type: (HttpRequest, HttpResponse) -> None
73+
"""
74+
Add correct attributes for a http client span.
75+
76+
:param request: The request made
77+
:type request: HttpRequest
78+
:param response: The response received by the server. Is None if no response received.
79+
:type response: HttpResponse
80+
"""
81+
pass
82+
6983
@property
7084
def span_instance(self):
7185
# type: () -> Any

sdk/core/azure-core/azure/core/tracing/ext/opencensus_span.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""Implements azure.core.tracing.AbstractSpan to wrap opencensus spans."""
66

77
from opencensus.trace import Span, execution_context
8+
from opencensus.trace.span import SpanKind
89
from opencensus.trace.link import Link
910
from opencensus.trace.propagation import trace_context_http_header_format
1011

@@ -14,7 +15,9 @@
1415
TYPE_CHECKING = False
1516

1617
if TYPE_CHECKING:
17-
from typing import Dict, Optional, Union
18+
from typing import Dict, Optional, Union, TypeVar
19+
20+
from azure.core.pipeline.transport import HttpRequest, HttpResponse
1821

1922

2023
class OpenCensusSpan(object):
@@ -33,8 +36,13 @@ def __init__(self, span=None, name="span"):
3336
"""
3437
if not span:
3538
tracer = self.get_current_tracer()
36-
span = tracer.span(name=name) # type: Span
39+
span = tracer.span(name=name) # type: Span
3740
self._span_instance = span
41+
self._span_component = "component"
42+
self._http_user_agent = "http.user_agent"
43+
self._http_method = "http.method"
44+
self._http_url = "http.url"
45+
self._http_status_code = "http.status_code"
3846

3947
@property
4048
def span_instance(self):
@@ -89,6 +97,28 @@ def add_attribute(self, key, value):
8997
"""
9098
self.span_instance.add_attribute(key, value)
9199

100+
def set_http_attributes(self, request, response=None):
101+
# type: (HttpRequest, Optional[HttpResponse]) -> None
102+
"""
103+
Add correct attributes for a http client span.
104+
105+
:param request: The request made
106+
:type request: HttpRequest
107+
:param response: The response received by the server. Is None if no response received.
108+
:type response: HttpResponse
109+
"""
110+
self._span_instance.span_kind = SpanKind.CLIENT
111+
self.span_instance.add_attribute(self._span_component, "http")
112+
self.span_instance.add_attribute(self._http_method, request.method)
113+
self.span_instance.add_attribute(self._http_url, request.url)
114+
user_agent = request.headers.get("User-Agent")
115+
if user_agent:
116+
self.span_instance.add_attribute(self._http_user_agent, user_agent)
117+
if response:
118+
self._span_instance.add_attribute(self._http_status_code, response.status_code)
119+
else:
120+
self._span_instance.add_attribute(self._http_status_code, 504)
121+
92122
@classmethod
93123
def link(cls, headers):
94124
# type: (Dict[str, str]) -> None

sdk/core/azure-core/tests/test_tracing_implementations.py

Lines changed: 25 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -13,65 +13,14 @@
1313

1414
from azure.core.tracing.ext.opencensus_span import OpenCensusSpan
1515
from opencensus.trace import tracer as tracer_module
16+
from opencensus.trace.span import SpanKind
1617
from opencensus.trace.samplers import AlwaysOnSampler
1718
from opencensus.trace.base_exporter import Exporter
1819
from opencensus.common.utils import timestamp_to_microseconds
20+
from tracing_common import MockExporter, ContextHelper
1921
import os
2022

2123

22-
class Node:
23-
def __init__(self, span_data):
24-
self.span_data = span_data # type: SpanData
25-
self.parent = None
26-
self.children = []
27-
28-
29-
class MockExporter(Exporter):
30-
def __init__(self):
31-
self.root = None
32-
self._all_nodes = []
33-
34-
def export(self, span_datas):
35-
# type: (List[SpanData]) -> None
36-
sp = span_datas[0] # type: SpanData
37-
node = Node(sp)
38-
if not node.span_data.parent_span_id:
39-
self.root = node
40-
self._all_nodes.append(node)
41-
42-
def build_tree(self):
43-
parent_dict = {}
44-
for node in self._all_nodes:
45-
parent_span_id = node.span_data.parent_span_id
46-
if parent_span_id not in parent_dict:
47-
parent_dict[parent_span_id] = []
48-
parent_dict[parent_span_id].append(node)
49-
50-
for node in self._all_nodes:
51-
if node.span_data.span_id in parent_dict:
52-
node.children = sorted(
53-
parent_dict[node.span_data.span_id], key=lambda x: timestamp_to_microseconds(x.span_data.start_time)
54-
)
55-
56-
57-
class ContextHelper(object):
58-
def __init__(self, environ={}):
59-
self.orig_tracer = OpenCensusSpan.get_current_tracer()
60-
self.orig_current_span = OpenCensusSpan.get_current_span()
61-
self.os_env = mock.patch.dict(os.environ, environ)
62-
63-
def __enter__(self):
64-
self.orig_tracer = OpenCensusSpan.get_current_tracer()
65-
self.orig_current_span = OpenCensusSpan.get_current_span()
66-
self.os_env.start()
67-
return self
68-
69-
def __exit__(self, exc_type, exc_val, exc_tb):
70-
OpenCensusSpan.set_current_tracer(self.orig_tracer)
71-
OpenCensusSpan.set_current_span(self.orig_current_span)
72-
self.os_env.stop()
73-
74-
7524
class TestOpencensusWrapper(unittest.TestCase):
7625
def test_span_passed_in(self):
7726
with ContextHelper():
@@ -162,3 +111,26 @@ def test_add_attribute(self):
162111
wrapped_class.add_attribute("test", "test2")
163112
assert wrapped_class.span_instance.attributes["test"] == "test2"
164113
assert parent.attributes["test"] == "test2"
114+
115+
def test_set_http_attributes(self):
116+
with ContextHelper():
117+
trace = tracer_module.Tracer(sampler=AlwaysOnSampler())
118+
parent = trace.start_span()
119+
wrapped_class = OpenCensusSpan(span=parent)
120+
request = mock.Mock()
121+
setattr(request, "method", "GET")
122+
setattr(request, "url", "some url")
123+
response = mock.Mock()
124+
setattr(request, "headers", {})
125+
setattr(response, "status_code", 200)
126+
wrapped_class.set_http_attributes(request)
127+
assert wrapped_class.span_instance.span_kind == SpanKind.CLIENT
128+
assert wrapped_class.span_instance.attributes.get("http.method") == request.method
129+
assert wrapped_class.span_instance.attributes.get("component") == "http"
130+
assert wrapped_class.span_instance.attributes.get("http.url") == request.url
131+
assert wrapped_class.span_instance.attributes.get("http.status_code") == 504
132+
assert wrapped_class.span_instance.attributes.get("http.user_agent") is None
133+
request.headers["User-Agent"] = "some user agent"
134+
wrapped_class.set_http_attributes(request, response)
135+
assert wrapped_class.span_instance.attributes.get("http.status_code") == response.status_code
136+
assert wrapped_class.span_instance.attributes.get("http.user_agent") == request.headers.get("User-Agent")

0 commit comments

Comments
 (0)