Skip to content

Commit 2f9e93b

Browse files
authored
[Core] Add native tracing live tests (#39945)
This validates tracing through the Azure storage blob sdk. Signed-off-by: Paul Van Eck <[email protected]>
1 parent 12e1435 commit 2f9e93b

File tree

5 files changed

+228
-0
lines changed

5 files changed

+228
-0
lines changed

sdk/core/azure-core/dev_requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ azure-storage-blob
1010
azure-data-tables
1111
opentelemetry-sdk~=1.26
1212
opentelemetry-instrumentation-requests>=0.50b0
13+
../../identity/azure-identity
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# ------------------------------------
2+
# Copyright (c) Microsoft Corporation.
3+
# Licensed under the MIT License.
4+
# ------------------------------------
5+
import pytest
6+
import os
7+
from typing import Any
8+
9+
from azure.storage.blob.aio import BlobServiceClient
10+
from opentelemetry.trace import SpanKind, StatusCode
11+
from opentelemetry.sdk.trace import ReadableSpan
12+
from devtools_testutils import get_credential
13+
14+
15+
class TestTracingAsync:
16+
"""Test class for validating async distributed tracing functionality."""
17+
18+
@pytest.mark.live_test_only
19+
@pytest.mark.asyncio
20+
async def test_blob_service_client_tracing_async(self, tracing_helper) -> None:
21+
"""Test that tracing captures async client method calls and HTTP requests.
22+
23+
This test validates that the distributed tracing functionality properly
24+
captures spans when using the async Blob service client, ensuring parent-child
25+
relationships are maintained and attributes are correctly populated.
26+
"""
27+
account_url = os.environ.get("AZURE_STORAGE_BLOB_ENDPOINT")
28+
if not account_url:
29+
pytest.skip("AZURE_STORAGE_BLOB_ENDPOINT environment variable is not set.")
30+
31+
client = BlobServiceClient(account_url=account_url, credential=get_credential(is_async=True))
32+
33+
with tracing_helper.tracer.start_as_current_span(name="root") as parent:
34+
await client.get_service_properties()
35+
36+
# Close the client explicitly
37+
await client.close()
38+
39+
spans = tracing_helper.exporter.get_finished_spans()
40+
span_names_list = [span.name for span in spans]
41+
# Check that last 3 spans are the expected ones (auth-related ones may vary)
42+
assert span_names_list[-3:] == ["GET", "BlobServiceClient.get_service_properties", "root"]
43+
44+
http_span: ReadableSpan = spans[-3]
45+
assert http_span.kind == SpanKind.CLIENT
46+
assert http_span.parent
47+
assert http_span.parent.span_id == spans[-2].context.span_id
48+
49+
# Validate HTTP span attributes
50+
assert http_span.attributes
51+
assert http_span.attributes["http.request.method"] == "GET"
52+
assert http_span.attributes["url.full"]
53+
assert http_span.attributes["server.address"]
54+
assert http_span.attributes["http.response.status_code"] == 200
55+
user_agent = http_span.attributes.get("user_agent.original", "")
56+
assert isinstance(user_agent, str) and "storage" in user_agent
57+
58+
# Validate method span
59+
method_span: ReadableSpan = spans[-2]
60+
assert method_span.kind == SpanKind.INTERNAL
61+
assert method_span.parent
62+
assert method_span.parent.span_id == spans[-1].context.span_id
63+
64+
@pytest.mark.live_test_only
65+
@pytest.mark.asyncio
66+
async def test_error_handling_with_tracing_async(self, tracing_helper) -> None:
67+
"""Test that tracing properly captures error information in async operations.
68+
69+
This test validates that when exceptions occur during async operations,
70+
the spans correctly capture the error information.
71+
"""
72+
account_name = "nonexistentaccount"
73+
invalid_url = f"https://{account_name}.blob.core.windows.net/"
74+
75+
client = BlobServiceClient(account_url=invalid_url, credential=get_credential(is_async=True))
76+
77+
# Expecting this operation to fail
78+
with tracing_helper.tracer.start_as_current_span(name="root") as parent:
79+
try:
80+
await client.get_service_properties()
81+
except Exception:
82+
# We expect an exception but want to verify the spans
83+
pass
84+
85+
# Close the client explicitly
86+
await client.close()
87+
88+
spans = tracing_helper.exporter.get_finished_spans()
89+
span_names_list = [span.name for span in spans]
90+
# Check that last 3 spans are the expected ones (auth-related ones may vary)
91+
assert span_names_list[-3:] == ["GET", "BlobServiceClient.get_service_properties", "root"]
92+
93+
http_span: ReadableSpan = spans[-3]
94+
assert http_span.kind == SpanKind.CLIENT
95+
assert http_span.attributes
96+
assert http_span.attributes["error.type"] == "azure.core.exceptions.ServiceRequestError"
97+
assert http_span.status.status_code == StatusCode.ERROR
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# ------------------------------------
2+
# Copyright (c) Microsoft Corporation.
3+
# Licensed under the MIT License.
4+
# ------------------------------------
5+
import pytest
6+
import os
7+
8+
from azure.storage.blob import BlobServiceClient
9+
from opentelemetry.trace import SpanKind, StatusCode
10+
from opentelemetry.sdk.trace import ReadableSpan
11+
from devtools_testutils import get_credential
12+
13+
14+
class TestTracing:
15+
16+
@pytest.mark.live_test_only
17+
def test_blob_service_client_tracing(self, tracing_helper):
18+
"""Test that tracing captures client method calls and HTTP requests.
19+
20+
This test validates that the distributed tracing functionality properly
21+
captures spans when using the Blob service client, ensuring parent-child
22+
relationships are maintained and attributes are correctly populated.
23+
"""
24+
account_url = os.environ.get("AZURE_STORAGE_BLOB_ENDPOINT")
25+
if not account_url:
26+
pytest.skip("AZURE_STORAGE_BLOB_ENDPOINT environment variable is not set.")
27+
28+
client = BlobServiceClient(account_url=account_url, credential=get_credential())
29+
30+
with tracing_helper.tracer.start_as_current_span(name="root") as parent:
31+
client.get_service_properties()
32+
33+
spans = tracing_helper.exporter.get_finished_spans()
34+
span_names_list = [span.name for span in spans]
35+
# Check that last 3 spans are the expected ones (auth-related ones may vary)
36+
assert span_names_list[-3:] == ["GET", "BlobServiceClient.get_service_properties", "root"]
37+
38+
http_span: ReadableSpan = spans[-3]
39+
assert http_span.kind == SpanKind.CLIENT
40+
assert http_span.parent
41+
assert http_span.parent.span_id == spans[-2].context.span_id
42+
43+
assert http_span.attributes
44+
assert http_span.attributes["http.request.method"] == "GET"
45+
assert http_span.attributes["url.full"]
46+
assert http_span.attributes["server.address"]
47+
assert http_span.attributes["http.response.status_code"] == 200
48+
user_agent = http_span.attributes.get("user_agent.original", "")
49+
assert isinstance(user_agent, str) and "storage" in user_agent
50+
51+
method_span: ReadableSpan = spans[-2]
52+
assert method_span.kind == SpanKind.INTERNAL
53+
assert method_span.parent
54+
assert method_span.parent.span_id == spans[-1].context.span_id
55+
56+
@pytest.mark.live_test_only
57+
def test_error_handling_with_tracing(self, tracing_helper) -> None:
58+
"""Test that tracing properly captures error information in operations.
59+
60+
This test validates that when exceptions occur during operations,
61+
the spans correctly capture the error information.
62+
63+
:param tracing_helper: Helper fixture that provides tracing functionality
64+
:type tracing_helper: Any
65+
"""
66+
account_name = "nonexistentaccount"
67+
invalid_url = f"https://{account_name}.blob.core.windows.net/"
68+
69+
client = BlobServiceClient(account_url=invalid_url, credential=get_credential())
70+
71+
# Expecting this operation to fail
72+
with tracing_helper.tracer.start_as_current_span(name="root") as parent:
73+
try:
74+
client.get_service_properties()
75+
except Exception:
76+
# We expect an exception but want to verify the spans
77+
pass
78+
79+
spans = tracing_helper.exporter.get_finished_spans()
80+
span_names_list = [span.name for span in spans]
81+
# Check that last 3 spans are the expected ones (auth-related ones may vary)
82+
assert span_names_list[-3:] == ["GET", "BlobServiceClient.get_service_properties", "root"]
83+
84+
http_span: ReadableSpan = spans[-3]
85+
assert http_span.kind == SpanKind.CLIENT
86+
assert http_span.attributes
87+
assert http_span.attributes["error.type"] == "azure.core.exceptions.ServiceRequestError"
88+
assert http_span.status.status_code == StatusCode.ERROR

sdk/core/test-resources.bicep

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
@minLength(6)
2+
@maxLength(21)
3+
@description('The base resource name.')
4+
param baseName string = resourceGroup().name
5+
6+
@description('Which Azure Region to deploy the resource to. Defaults to the resource group location.')
7+
param location string = resourceGroup().location
8+
9+
@description('The client OID to grant access to test resources.')
10+
param testApplicationOid string
11+
12+
// Storage Blob Data Contributor
13+
var blobDataContributor = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
14+
15+
resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = {
16+
name: '${baseName}sa'
17+
location: location
18+
kind: 'StorageV2'
19+
sku: {
20+
name: 'Standard_LRS'
21+
}
22+
properties: {
23+
accessTier: 'Hot'
24+
}
25+
}
26+
27+
// Role assignment for the identity to access the storage account
28+
resource storageRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
29+
name: guid(storageAccount.id, testApplicationOid, blobDataContributor)
30+
scope: storageAccount
31+
properties: {
32+
principalId: testApplicationOid
33+
roleDefinitionId: blobDataContributor
34+
principalType: 'ServicePrincipal'
35+
}
36+
}
37+
38+
output AZURE_STORAGE_ACCOUNT_NAME string = storageAccount.name
39+
output AZURE_STORAGE_BLOB_ENDPOINT string = storageAccount.properties.primaryEndpoints.blob

sdk/core/tests.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,6 @@ extends:
55
parameters:
66
ServiceDirectory: core
77
BuildTargetingString: '*'
8+
EnvVars:
9+
AZURE_TEST_RUN_LIVE: 'true'
10+
AZURE_SKIP_LIVE_RECORDING: 'true'

0 commit comments

Comments
 (0)