Skip to content

Commit 16bcf0a

Browse files
authored
Add TelemetryPolicy (#210)
* Add TelemetryPolicy * Satisfy `cargo fmt` * Resolve PR feedback * Satisfy `cargo fmt` * Add tests * Change how pipelines are created Also introduces base ClientOptions, which all our other SDKs have. * Format the code again * Resolve PR feedback
1 parent bc7faed commit 16bcf0a

File tree

9 files changed

+209
-17
lines changed

9 files changed

+209
-17
lines changed

sdk/core/Cargo.toml

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,27 @@ categories = ["api-bindings"]
1313
edition = "2018"
1414

1515
[dependencies]
16+
async-trait = "0.1"
17+
bytes = "1.0"
1618
chrono = "0.4"
17-
http = "0.2"
19+
dyn-clone = "1.0"
1820
futures = "0.3"
21+
http = "0.2"
1922
hyper = { version = "0.14", optional = true }
23+
hyper-rustls = { version = "0.22", optional = true }
2024
log = "0.4"
21-
thiserror = "1.0"
25+
oauth2 = "4.0.0"
26+
rand = "0.7"
27+
reqwest = { version = "0.11", features = ["stream"], optional = true }
2228
serde = "1.0"
2329
serde_derive = "1.0"
2430
serde_json = "1.0"
31+
thiserror = "1.0"
2532
url = "2.2"
2633
uuid = { version = "0.8" }
27-
bytes = "1.0"
28-
hyper-rustls = { version = "0.22", optional = true }
29-
async-trait = "0.1"
30-
oauth2 = "4.0.0"
31-
reqwest = { version = "0.11", features = ["stream"], optional = true }
32-
rand = "0.7"
33-
dyn-clone = "1.0"
34+
35+
[build-dependencies]
36+
rustc_version = "0.3.3"
3437

3538
[dev-dependencies]
3639
env_logger = "0.8"

sdk/core/build.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
use rustc_version::version;
2+
3+
fn main() {
4+
let version = match version() {
5+
Ok(version) => version.to_string(),
6+
Err(_) => "unknown".to_owned(),
7+
};
8+
println!("cargo:rustc-env=AZSDK_RUSTC_VERSION={}", version);
9+
}

sdk/core/src/client_options.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
use crate::policies::{Policy, TelemetryOptions};
2+
use std::sync::Arc;
3+
4+
/// Options passed clients to customer policies, telemetry, etc.
5+
#[derive(Clone, Debug, Default)]
6+
pub struct ClientOptions {
7+
// TODO: Expose retry options and transport overrides.
8+
pub per_call_policies: Vec<Arc<dyn Policy>>,
9+
pub per_retry_policies: Vec<Arc<dyn Policy>>,
10+
11+
pub telemetry: TelemetryOptions,
12+
}

sdk/core/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ extern crate serde_derive;
1010
mod macros;
1111

1212
mod bytes_stream;
13+
pub mod client_options;
1314
mod constants;
1415
mod context;
1516
mod errors;
@@ -35,6 +36,7 @@ use std::fmt::Debug;
3536
use uuid::Uuid;
3637

3738
pub use bytes_stream::*;
39+
pub use client_options::ClientOptions;
3840
pub use constants::*;
3941
pub use context::Context;
4042
pub use errors::*;

sdk/core/src/pipeline.rs

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
1-
use crate::policies::{Policy, PolicyResult};
2-
use crate::{Context, Request, Response};
1+
use crate::policies::{Policy, PolicyResult, TelemetryPolicy};
2+
use crate::{ClientOptions, Context, Request, Response};
33
use std::sync::Arc;
44

55
/// Execution pipeline.
66
///
77
/// A pipeline follows a precise flow:
88
///
9-
/// 1. Per call policies are executed. Per call policies can fail and bail out of the pipeline
9+
/// 1. Client library-specified per-call policies are executed. Per-call policies can fail and bail out of the pipeline
1010
/// immediately.
11-
/// 2. Retry policy. It allows to reexecute the following policies.
12-
/// 3. Per retry policies. Per retry polices are always executed at least once but are reexecuted
11+
/// 2. User-specified per-call policies are executed.
12+
/// 3. Telemetry policy.
13+
/// 4. Retry policy. It allows to re-execute the following policies.
14+
/// 5. Client library-specified per-retry policies. Per-retry polices are always executed at least once but are re-executed
1315
/// in case of retries.
14-
/// 4. Transport policy. Transtport policy is always the last policy and is the policy that
16+
/// 6. User-specified per-retry policies are executed.
17+
/// 7. Transport policy. Transport policy is always the last policy and is the policy that
1518
/// actually constructs the `Response` to be passed up the pipeline.
1619
///
1720
/// A pipeline is immutable. In other words a policy can either succeed and call the following
@@ -24,18 +27,38 @@ pub struct Pipeline {
2427
}
2528

2629
impl Pipeline {
30+
/// Creates a new pipeline given the client library crate name and version,
31+
/// alone with user-specified and client library-specified policies.
32+
///
33+
/// Crates can simply pass `option_env!("CARGO_PKG_NAME")` and `option_env!("CARGO_PKG_VERSION")` for the
34+
/// `crate_name` and `crate_version` arguments respectively.
2735
pub fn new(
36+
crate_name: Option<&'static str>,
37+
crate_version: Option<&'static str>,
38+
options: &ClientOptions,
2839
per_call_policies: Vec<Arc<dyn Policy>>,
2940
retry: Arc<dyn Policy>,
3041
per_retry_policies: Vec<Arc<dyn Policy>>,
3142
transport_policy: Arc<dyn Policy>,
3243
) -> Self {
33-
let mut pipeline =
34-
Vec::with_capacity(per_call_policies.len() + per_retry_policies.len() + 2);
44+
let mut pipeline: Vec<Arc<dyn Policy>> = Vec::with_capacity(
45+
options.per_call_policies.len()
46+
+ per_call_policies.len()
47+
+ options.per_retry_policies.len()
48+
+ per_retry_policies.len()
49+
+ 3,
50+
);
3551

3652
pipeline.extend_from_slice(&per_call_policies);
53+
pipeline.extend_from_slice(&options.per_call_policies);
54+
pipeline.push(Arc::new(TelemetryPolicy::new(
55+
crate_name,
56+
crate_version,
57+
&options.telemetry,
58+
)));
3759
pipeline.push(retry);
3860
pipeline.extend_from_slice(&per_retry_policies);
61+
pipeline.extend_from_slice(&options.per_retry_policies);
3962
pipeline.push(transport_policy);
4063

4164
Self { pipeline }

sdk/core/src/policies/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
mod retry_policies;
2+
mod telemetry_policy;
23
mod transport;
34

45
use crate::{Context, Request, Response};
56
pub use retry_policies::*;
67
use std::error::Error;
78
use std::sync::Arc;
9+
pub use telemetry_policy::*;
810
pub use transport::*;
911

1012
pub type PolicyResult<T> = Result<T, Box<dyn Error + Send + Sync>>;
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
use crate::policies::{Policy, PolicyResult};
2+
use crate::{Context, Request, Response};
3+
4+
use http::{header::USER_AGENT, HeaderValue};
5+
use std::env::consts::{ARCH, OS};
6+
use std::sync::Arc;
7+
8+
#[derive(Clone, Debug, Default)]
9+
pub struct TelemetryOptions {
10+
pub application_id: Option<String>,
11+
}
12+
13+
#[derive(Clone, Debug)]
14+
pub struct TelemetryPolicy {
15+
header: String,
16+
}
17+
18+
/// Sets the User-Agent header with useful information in a typical format for Azure SDKs.
19+
///
20+
/// Client libraries should create a `TelemetryPolicy` using `option_env!()` like so:
21+
/// ```
22+
/// use azure_core::policies::{TelemetryOptions, TelemetryPolicy};
23+
/// let policy = TelemetryPolicy::new(option_env!("CARGO_PKG_NAME"), option_env!("CARGO_PKG_VERSION"), &TelemetryOptions::default());
24+
/// ```
25+
impl<'a> TelemetryPolicy {
26+
pub fn new(
27+
crate_name: Option<&'a str>,
28+
crate_version: Option<&'a str>,
29+
options: &TelemetryOptions,
30+
) -> Self {
31+
Self::new_with_rustc_version(
32+
crate_name,
33+
crate_version,
34+
option_env!("AZSDK_RUSTC_VERSION"),
35+
options,
36+
)
37+
}
38+
39+
fn new_with_rustc_version(
40+
crate_name: Option<&'a str>,
41+
crate_version: Option<&'a str>,
42+
rustc_version: Option<&'a str>,
43+
options: &TelemetryOptions,
44+
) -> Self {
45+
const UNKNOWN: &'static str = "unknown";
46+
let mut crate_name = crate_name.unwrap_or(UNKNOWN);
47+
let crate_version = crate_version.unwrap_or(UNKNOWN);
48+
let rustc_version = rustc_version.unwrap_or(UNKNOWN);
49+
let platform_info = format!("({}; {}; {})", rustc_version, OS, ARCH,);
50+
51+
if let Some(name) = crate_name.strip_prefix("azure_") {
52+
crate_name = name;
53+
}
54+
55+
let header = match &options.application_id {
56+
Some(application_id) => format!(
57+
"{} azsdk-rust-{}/{} {}",
58+
application_id, crate_name, crate_version, platform_info
59+
),
60+
None => format!(
61+
"azsdk-rust-{}/{} {}",
62+
crate_name, crate_version, platform_info
63+
),
64+
};
65+
66+
TelemetryPolicy { header: header }
67+
}
68+
}
69+
70+
#[async_trait::async_trait]
71+
impl Policy for TelemetryPolicy {
72+
async fn send(
73+
&self,
74+
ctx: &mut Context,
75+
request: &mut Request,
76+
next: &[Arc<dyn Policy>],
77+
) -> PolicyResult<Response> {
78+
request
79+
.headers_mut()
80+
.insert(USER_AGENT, HeaderValue::from_str(&self.header)?);
81+
82+
next[0].send(ctx, request, &next[1..]).await
83+
}
84+
}
85+
86+
#[cfg(test)]
87+
mod test {
88+
use super::*;
89+
90+
#[test]
91+
fn test_without_application_id() {
92+
let policy = TelemetryPolicy::new_with_rustc_version(
93+
Some("azure_test"), // Tests that "azure_" is removed.
94+
Some("1.2.3"),
95+
Some("4.5.6"),
96+
&TelemetryOptions::default(),
97+
);
98+
assert_eq!(
99+
policy.header,
100+
format!("azsdk-rust-test/1.2.3 (4.5.6; {}; {})", OS, ARCH)
101+
);
102+
}
103+
104+
#[test]
105+
fn test_with_application_id() {
106+
let options = TelemetryOptions {
107+
application_id: Some("my_app".to_string()),
108+
};
109+
let policy = TelemetryPolicy::new_with_rustc_version(
110+
Some("test"),
111+
Some("1.2.3"),
112+
Some("4.5.6"),
113+
&options,
114+
);
115+
assert_eq!(
116+
policy.header,
117+
format!("my_app azsdk-rust-test/1.2.3 (4.5.6; {}; {})", OS, ARCH)
118+
);
119+
}
120+
121+
#[test]
122+
fn test_missing_env() {
123+
// Would simulate if option_env!("CARGO_PKG_NAME"), for example, returned None.
124+
let policy =
125+
TelemetryPolicy::new_with_rustc_version(None, None, None, &TelemetryOptions::default());
126+
assert_eq!(
127+
policy.header,
128+
format!("azsdk-rust-unknown/unknown (unknown; {}; {})", OS, ARCH)
129+
)
130+
}
131+
}

sdk/core/src/request.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ impl Request {
4545
&self.headers
4646
}
4747

48+
pub fn headers_mut(&mut self) -> &mut HeaderMap {
49+
&mut self.headers
50+
}
51+
4852
pub fn body(&self) -> &Body {
4953
&self.body
5054
}

sdk/cosmos/src/clients/cosmos_client.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use crate::resources::permission::AuthorizationToken;
55
use crate::resources::ResourceType;
66
use crate::{requests, ReadonlyString};
77

8+
use azure_core::client_options::ClientOptions;
89
use azure_core::pipeline::Pipeline;
910
use azure_core::policies::{LinearRetryPolicy, Policy, TransportOptions, TransportPolicy};
1011
use azure_core::Context;
@@ -33,6 +34,7 @@ pub struct CosmosClient {
3334
}
3435
/// Options for specifying how a Cosmos client will behave
3536
pub struct CosmosOptions {
37+
options: ClientOptions,
3638
retry: Arc<dyn Policy>,
3739
transport: TransportOptions,
3840
}
@@ -41,6 +43,7 @@ impl CosmosOptions {
4143
/// Create options based on the provided http client
4244
pub fn with_client(client: Arc<dyn HttpClient>) -> Self {
4345
Self {
46+
options: ClientOptions::default(),
4447
retry: Arc::new(LinearRetryPolicy::default()), // this defaults to linear backoff
4548
transport: TransportOptions::new(client),
4649
}
@@ -53,6 +56,9 @@ fn new_pipeline_from_options(options: CosmosOptions) -> Pipeline {
5356
let per_retry_policies = Vec::new();
5457
let transport_policy = TransportPolicy::new(options.transport);
5558
Pipeline::new(
59+
option_env!("CARGO_PKG_NAME"),
60+
option_env!("CARGO_PKG_VERSION"),
61+
&options.options,
5662
per_call_policies,
5763
options.retry,
5864
per_retry_policies,

0 commit comments

Comments
 (0)