Skip to content

Commit

Permalink
[Tracing] Add error.type attribute to spans (#34619)
Browse files Browse the repository at this point in the history
If an exception is raised, the fully-qualified name of the exception is
now stored in the error.type span attribute.

Signed-off-by: Paul Van Eck <[email protected]>
  • Loading branch information
pvaneck authored Apr 4, 2024
1 parent 9e5c2e0 commit acfb396
Show file tree
Hide file tree
Showing 6 changed files with 55 additions and 0 deletions.
2 changes: 2 additions & 0 deletions sdk/core/azure-core-tracing-opentelemetry/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Features Added

- If a span exits with an exception, the exception name is now recorded in the `error.type` attribute. ([#34619](https://github.com/Azure/azure-sdk-for-python/pull/34619))

### Breaking Changes

- Remapped certain attributes to converge with OpenTelemetry semantic conventions version `1.23.1` ([#34089](https://github.com/Azure/azure-sdk-for-python/pull/34089)):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@

_SUPPRESSED_SPAN_FLAG = "SUPPRESSED_SPAN_FLAG"
_LAST_UNSUPPRESSED_SPAN = "LAST_UNSUPPRESSED_SPAN"
_ERROR_SPAN_ATTRIBUTE = "error.type"


class OpenTelemetrySpan(HttpSpanMixin, object):
Expand Down Expand Up @@ -226,6 +227,10 @@ def __enter__(self) -> "OpenTelemetrySpan":

def __exit__(self, exception_type, exception_value, traceback) -> None:
# Finish the span.
if exception_type:
module = exception_type.__module__ if exception_type.__module__ != "builtins" else ""
error_type = f"{module}.{exception_type.__qualname__}" if module else exception_type.__qualname__
self.add_attribute(_ERROR_SPAN_ATTRIBUTE, error_type)
if self._current_ctxt_manager:
self._current_ctxt_manager.__exit__(exception_type, exception_value, traceback) # pylint: disable=no-member
self._current_ctxt_manager = None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import requests


from azure.core.exceptions import ClientAuthenticationError
from azure.core.tracing.ext.opentelemetry_span import OpenTelemetrySpan
from azure.core.tracing import SpanKind, AbstractSpan
from azure.core import __version__ as core_version
Expand Down Expand Up @@ -369,3 +370,20 @@ def test_span_kind(self, tracing_helper):

with pytest.raises(ValueError):
wrapped_class.kind = "somethingstuid"

def test_error_type_attribute_builtin_error(self, tracing_helper):
with tracing_helper.tracer.start_as_current_span("Root") as parent:
with pytest.raises(ValueError):
with OpenTelemetrySpan() as wrapped_class:
raise ValueError("This is a test error")
assert wrapped_class.span_instance.attributes.get("error.type") == "ValueError"

def test_error_type_attribute_azure_error(self, tracing_helper):
with tracing_helper.tracer.start_as_current_span("Root") as parent:
with pytest.raises(ClientAuthenticationError):
with OpenTelemetrySpan() as wrapped_class:
raise ClientAuthenticationError("This is a test error")
assert (
wrapped_class.span_instance.attributes.get("error.type")
== "azure.core.exceptions.ClientAuthenticationError"
)
2 changes: 2 additions & 0 deletions sdk/core/azure-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

### Other Changes

- HTTP tracing spans will now include an `error.type` attribute if an error status code is returned. #34619

## 1.30.1 (2024-02-29)

### Other Changes
Expand Down
4 changes: 4 additions & 0 deletions sdk/core/azure-core/azure/core/tracing/_abstract_span.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ class HttpSpanMixin:
_HTTP_STATUS_CODE = "http.status_code"
_NET_PEER_NAME = "net.peer.name"
_NET_PEER_PORT = "net.peer.port"
_ERROR_TYPE = "error.type"

def set_http_attributes(
self: AbstractSpan, request: HttpRequestType, response: Optional[HttpResponseType] = None
Expand Down Expand Up @@ -289,8 +290,11 @@ def set_http_attributes(
self.add_attribute(HttpSpanMixin._HTTP_USER_AGENT, user_agent)
if response and response.status_code:
self.add_attribute(HttpSpanMixin._HTTP_STATUS_CODE, response.status_code)
if response.status_code >= 400:
self.add_attribute(HttpSpanMixin._ERROR_TYPE, str(response.status_code))
else:
self.add_attribute(HttpSpanMixin._HTTP_STATUS_CODE, 504)
self.add_attribute(HttpSpanMixin._ERROR_TYPE, "504")


class Link:
Expand Down
24 changes: 24 additions & 0 deletions sdk/core/azure-core/tests/test_tracing_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def test_distributed_tracing_policy_solo(http_request, http_response):
assert network_span.attributes.get("x-ms-request-id") == "some request id"
assert network_span.attributes.get("x-ms-client-request-id") == "some client request id"
assert network_span.attributes.get("http.status_code") == 202
assert "error.type" not in network_span.attributes

# Check on_exception
network_span = root_span.children[1]
Expand All @@ -68,6 +69,29 @@ def test_distributed_tracing_policy_solo(http_request, http_response):
assert network_span.attributes.get("http.user_agent") is None
assert network_span.attributes.get("x-ms-request-id") == None
assert network_span.attributes.get("http.status_code") == 504
assert network_span.attributes.get("error.type")


@pytest.mark.parametrize("http_request,http_response", request_and_responses_product(HTTP_RESPONSES))
def test_distributed_tracing_policy_error_response(http_request, http_response):
"""Test policy when the HTTP response corresponds to an error."""
settings.tracing_implementation.set_value(FakeSpan)
with FakeSpan(name="parent") as root_span:
policy = DistributedTracingPolicy(tracing_attributes={"myattr": "myvalue"})

request = http_request("GET", "http://localhost/temp?query=query")

pipeline_request = PipelineRequest(request, PipelineContext(None))
policy.on_request(pipeline_request)

response = create_http_response(http_response, request, None)
response.headers = request.headers
response.status_code = 403

policy.on_response(pipeline_request, PipelineResponse(request, response, PipelineContext(None)))
network_span = root_span.children[0]
assert network_span.name == "/temp"
assert network_span.attributes.get("error.type") == "403"


@pytest.mark.parametrize("http_request,http_response", request_and_responses_product(HTTP_RESPONSES))
Expand Down

0 comments on commit acfb396

Please sign in to comment.