Skip to content

Commit abf685b

Browse files
Added ability to test distributed tracing functionality. (#2896)
Fixes #2769 - Adds ability to easily test distributed tracing. Also adds a test example for KeyVault Secrets APIs. This PR introduces a new `azure_core_test` API named `assert_instrumentation_information` whose purpose is to ensure that the distributed tracing information is correctly generated for a given service client implementation. The API takes three pieces of information: A capture which creates a service client with a provided distributed tracing `TracerProvider`, a capture which actually performs the test and an `InstrumentationInformation` structure which describes the service calling the API and the particular service client APIs which are expected to have been called during the test. It then analyzes the actual traces generated during the test and verifies that the expected service client calls were made and that the expected distributed tracing attributes have been generated. --------- Co-authored-by: Heath Stewart <[email protected]>
1 parent 1e57604 commit abf685b

File tree

7 files changed

+572
-100
lines changed

7 files changed

+572
-100
lines changed

doc/distributed-tracing-for-rust-service-clients.md

Lines changed: 128 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
<!-- Copyright(C) Microsoft Corp. All Rights Reserved. -->
2-
3-
<!-- cspell: ignore liudmila subclients -->
41
# Distributed tracing options in Rust service clients
52

63
## Distributed tracing fundamentals
@@ -149,7 +146,7 @@ to
149146
```diff
150147
pub struct MyServiceClient {
151148
endpoint: Url,
152-
+ tracer: std::sync::Arc<dyn azure_core::tracing::Tracer>,
149+
+ tracer: std::sync::Arc<dyn azure_core::tracing::Tracer>,
153150
}
154151
```
155152

@@ -192,7 +189,7 @@ pub fn new(
192189
Vec::default(),
193190
vec![auth_policy],
194191
),
195-
})
192+
})
196193
}
197194
```
198195

@@ -217,33 +214,33 @@ pub fn new(
217214
credential,
218215
vec!["https://vault.azure.net/.default"],
219216
));
220-
+ let tracer =
221-
+ if let Some(tracer_options) = &options.client_options.instrumentation {
222-
+ tracer_options
223-
+ .tracer_provider
224-
+ .as_ref()
225-
+ .map(|tracer_provider| {
226-
+ tracer_provider.get_tracer(
227-
+ Some(#client_namespace),
228-
+ option_env!("CARGO_PKG_NAME").unwrap_or("UNKNOWN"),
229-
+ option_env!("CARGO_PKG_VERSION").unwrap_or("UNKNOWN"),
230-
+ )
231-
+ })
232-
+ } else {
233-
+ None
234-
+ };
217+
+ let tracer =
218+
+ if let Some(tracer_options) = &options.client_options.instrumentation {
219+
+ tracer_options
220+
+ .tracer_provider
221+
+ .as_ref()
222+
+ .map(|tracer_provider| {
223+
+ tracer_provider.get_tracer(
224+
+ Some(#client_namespace),
225+
+ option_env!("CARGO_PKG_NAME").unwrap_or("UNKNOWN"),
226+
+ option_env!("CARGO_PKG_VERSION").unwrap_or("UNKNOWN"),
227+
+ )
228+
+ })
229+
+ } else {
230+
+ None
231+
+ };
235232
Ok(Self {
236-
+ tracer,
237-
endpoint,
238-
api_version: options.api_version,
239-
pipeline: Pipeline::new(
240-
option_env!("CARGO_PKG_NAME"),
241-
option_env!("CARGO_PKG_VERSION"),
242-
options.client_options,
243-
Vec::default(),
244-
vec![auth_policy],
245-
),
246-
})
233+
+ tracer,
234+
endpoint,
235+
api_version: options.api_version,
236+
pipeline: Pipeline::new(
237+
option_env!("CARGO_PKG_NAME"),
238+
option_env!("CARGO_PKG_VERSION"),
239+
options.client_options,
240+
Vec::default(),
241+
vec![auth_policy],
242+
),
243+
})
247244
}
248245
```
249246

@@ -297,19 +294,19 @@ pub async fn get(
297294
path: &str,
298295
options: Option<TestServiceClientGetMethodOptions<'_>>,
299296
) -> Result<RawResponse> {
300-
+ let options = {
301-
+ let mut options = options.unwrap_or_default();
302-
+ let public_api_info = azure_core::tracing::PublicApiInstrumentationInformation {
303-
+ api_name: "TestFunction",
304-
+ attributes: Vec::new(),
305-
+ };
306-
+ let mut ctx = options.method_options.context.with_value(public_api_info);
307-
+ if let Some(tracer) = &self.tracer {
308-
+ ctx = ctx.with_value(tracer.clone());
309-
+ }
310-
+ options.method_options.context = ctx;
311-
+ Some(options)
312-
+ };
297+
+ let options = {
298+
+ let mut options = options.unwrap_or_default();
299+
+ let public_api_info = azure_core::tracing::PublicApiInstrumentationInformation {
300+
+ api_name: "TestFunction",
301+
+ attributes: Vec::new(),
302+
+ };
303+
+ let mut ctx = options.method_options.context.with_value(public_api_info);
304+
+ if let Some(tracer) = &self.tracer {
305+
+ ctx = ctx.with_value(tracer.clone());
306+
+ }
307+
+ options.method_options.context = ctx;
308+
+ Some(options)
309+
+ };
313310
let mut url = self.endpoint.clone();
314311
url.set_path(path);
315312
url.query_pairs_mut()
@@ -319,8 +316,8 @@ pub async fn get(
319316

320317
let response = self
321318
.pipeline
322-
- .send(&options.method_options.context, &mut request)
323-
+ .send(&ctx, &mut request)
319+
- .send(&options.method_options.context, &mut request)
320+
+ .send(&ctx, &mut request)
324321
.await?;
325322
if !response.status().is_success() {
326323
return Err(azure_core::Error::message(
@@ -340,7 +337,7 @@ implement per-service-client distributed tracing attributes.
340337
The parameters for the `tracing::function` roughly follow the following BNF:
341338

342339
```bnf
343-
tracing_parameters = quoted_string [ ',' '( attribute_list ')']
340+
tracing_parameters = quoted_string [ ',' 'attributes' '=' '( attribute_list ')']
344341
quoted_string = `"` <characters> `"`
345342
attribute_list = attribute | attribute [`,`] attribute_list
346343
attribute = key '=' value
@@ -353,9 +350,9 @@ value = <any Rust expression>
353350
This means that the following are valid parameters for `tracing::function`:
354351

355352
* `#[tracing::function("MyServiceClient.MyApi")]` - specifies a public API name.
356-
* `#[tracing::function("Name", (az.namespace="namespace"))]` - specifies a public API name and creates a span with an attribute named "az.namespace" and a value of "namespace".
357-
* `#[tracing::function("Name", (api_count=23, "my_attribute" = "Abc"))]` - specifies a public API name and creates a span with two attributes, one named "api_count" with a value of "23" and the other with the name "my_attribute" and a value of "Abc"
358-
* `#[tracing::function("Name", ("API path"=path))]` - specifies a public API name and creates a span with an attribute named "API path" and the value of the parameter named "path".
353+
* `#[tracing::function("Name", attributes = (az.namespace="namespace"))]` - specifies a public API name and creates a span with an attribute named "az.namespace" and a value of "namespace".
354+
* `#[tracing::function("Name", attributes = (api_count=23, "my_attribute" = "Abc"))]` - specifies a public API name and creates a span with two attributes, one named "api_count" with a value of "23" and the other with the name "my_attribute" and a value of "Abc"
355+
* `#[tracing::function("Name", attributes = ("API path"=path))]` - specifies a public API name and creates a span with an attribute named "API path" and the value of the parameter named "path".
359356

360357
This allows a generator to pass in simple attribute annotations to the public API spans created by the pipeline.
361358

@@ -379,14 +376,14 @@ The client can then add whatever attributes to the span it needs, and after the
379376

380377
Note that in this model, the client is responsible for ending the span.
381378

379+
<!-- cspell:ignore subclients -->
382380
### Service implementations with "subclients"
383381

384382
Service clients can sometimes contain "subclients" - clients which have their own pipelines and endpoint which contain subclient specific functionality.
385383

386384
Such subclients often have an accessor function to create a new subclient instance which looks like this:
387385

388386
```rust
389-
390387
pub fn get_operation_templates_lro_client(&self) -> OperationTemplatesLroClient {
391388
OperationTemplatesLroClient {
392389
api_version: self.api_version.clone(),
@@ -412,3 +409,83 @@ pub fn get_operation_templates_lro_client(&self) -> OperationTemplatesLroClient
412409
```
413410

414411
This adds a clone of the parent client's `tracer` to the subclient - it functions similarly to `#[tracing::new]` but for subclient instantiation.
412+
413+
## Verifying distributed tracing support
414+
415+
The `azure_core_test::tracing` package provides functionality to allow developers to verify that their distributed tracing functionality is generating the expected tracing information.
416+
417+
This functionality is driven from the `azure_core_test::tracing::assert_instrumentation_information` API.
418+
419+
This function takes two closures and a structure which describes the expected API shape. The first closure is used to create an instance of the public API, the second actually executes the public API.
420+
421+
As an example, here is a test for the `keyvault_secrets` package:
422+
423+
```rust
424+
assert_instrumentation_information(
425+
// Create an instance of a SecretClient adding the instrumentation options to ensure distributed tracing is enabled.
426+
|tracer_provider| {
427+
let mut options = SecretClientOptions::default();
428+
recording.instrument(&mut options.client_options);
429+
options.client_options.instrumentation = Some(InstrumentationOptions {
430+
tracer_provider: Some(tracer_provider),
431+
});
432+
SecretClient::new(
433+
recording.var("AZURE_KEYVAULT_URL", None).as_str(),
434+
recording.credential(),
435+
Some(options),
436+
)
437+
},
438+
// Perform a series of tests against the newly created SecretClient.
439+
// In this case, there are two APIs called - set_secret and get_secret.
440+
|client: SecretClient| {
441+
Box::pin(async move {
442+
// Set a secret.
443+
let body = SetSecretParameters {
444+
value: Some("secret-value-instrument".into()),
445+
..Default::default()
446+
};
447+
let secret = client
448+
.set_secret("secret-roundtrip-instrument", body.try_into()?, None)
449+
.await?
450+
.into_body()
451+
.await?;
452+
assert_eq!(secret.value, Some("secret-value-instrument".into()));
453+
454+
// Get a specific version of a secret.
455+
let version = secret.resource_id()?.version.unwrap_or_default();
456+
let secret = client
457+
.get_secret("secret-roundtrip-instrument", version.as_ref(), None)
458+
.await?
459+
.into_body()
460+
.await?;
461+
assert_eq!(secret.value, Some("secret-value-instrument".into()));
462+
Ok(())
463+
})
464+
},
465+
// Verify that the tests above generated appropriate distributed traces.
466+
// First verify the package information and service client namespace.
467+
// Next verify that two service APIs were called - one with the language
468+
// independent name of "KeyVault.setSecret" (using the "PUT" HTTP verb),
469+
// and the other with a language independent name of "KeyVault.getSecret"
470+
// using the "get" verb.
471+
ExpectedInstrumentation {
472+
package_name: recording.var("CARGO_PKG_NAME", None),
473+
package_version: recording.var("CARGO_PKG_VERSION", None),
474+
package_namespace: Some("azure_security_keyvault_secrets"),
475+
api_calls: vec![
476+
ExpectedApiInformation {
477+
api_name: Some("KeyVault.setSecret"),
478+
api_verb: azure_core::http::Method::Put,
479+
..Default::default()
480+
},
481+
ExpectedApiInformation {
482+
api_name: Some("KeyVault.getSecret"),
483+
..Default::default()
484+
},
485+
],
486+
},
487+
)
488+
.await?;
489+
```
490+
491+
The first closure creates an instance of a KeyVault client. The second one executes a keyvault secrets round trip test setting a secret and retrieving the secret. In this case, the instrumentation information describes the name of the package and the namespace for the service client. It says that there will be two instrumented APIs being called - the first named `KeyVault.setSecret` and the second named `KeyVault.getSecret`.

sdk/core/azure_core/src/http/pipeline.rs

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,19 @@ impl Pipeline {
5757
let tracer = core_client_options
5858
.instrumentation
5959
.tracer_provider
60-
.as_ref()
6160
.map(|provider| {
61+
// Note that the choice to use "None" as the namespace here
62+
// is intentional.
63+
// The `azure_namespace` parameter is used to populate the `az.namespace`
64+
// span attribute, however that information is only known by the author of the
65+
// client library, not the core library.
66+
// It is also *not* a constant that can be derived from the crate information -
67+
// it is a value that is determined from the list of resource providers
68+
// listed [here](https://learn.microsoft.com/azure/azure-resource-manager/management/azure-services-resource-providers).
69+
//
70+
// This information can only come from the package owner. It doesn't make sense
71+
// to burden all users of the azure_core pipeline with determining this
72+
// information, so we use `None` here.
6273
provider.get_tracer(None, crate_name.unwrap_or("Unknown"), crate_version)
6374
});
6475

@@ -75,18 +86,6 @@ impl Pipeline {
7586

7687
let mut per_try_policies = per_try_policies.clone();
7788
if let Some(ref tracer) = tracer {
78-
// Note that the choice to use "None" as the namespace here
79-
// is intentional.
80-
// The `azure_namespace` parameter is used to populate the `az.namespace`
81-
// span attribute, however that information is only known by the author of the
82-
// client library, not the core library.
83-
// It is also *not* a constant that can be derived from the crate information -
84-
// it is a value that is determined from the list of resource providers
85-
// listed [here](https://learn.microsoft.com/azure/azure-resource-manager/management/azure-services-resource-providers).
86-
//
87-
// This information can only come from the package owner. It doesn't make sense
88-
// to burden all users of the azure_core pipeline with determining this
89-
// information, so we use `None` here.
9089
let request_instrumentation_policy =
9190
RequestInstrumentationPolicy::new(Some(tracer.clone()));
9291
push_unique(&mut per_try_policies, request_instrumentation_policy);
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"AssetsRepo": "Azure/azure-sdk-assets",
33
"AssetsRepoPrefixPath": "rust",
4-
"Tag": "rust/azure_core_opentelemetry_2bc5efc404",
4+
"Tag": "rust/azure_core_opentelemetry_341a93c45d",
55
"TagPrefix": "rust/azure_core_opentelemetry"
66
}

0 commit comments

Comments
 (0)