1use aws_smithy_types::config_bag::{Storable, StoreReplace};
7use aws_types::app_name::AppName;
8use aws_types::build_metadata::{OsFamily, BUILD_METADATA};
9use aws_types::os_shim_internal::Env;
10use std::borrow::Cow;
11use std::error::Error;
12use std::fmt;
13
14mod interceptor;
15mod metrics;
16#[cfg(feature = "test-util")]
17pub mod test_util;
18
19const USER_AGENT_VERSION: &str = "2.1";
20
21use crate::user_agent::metrics::BusinessMetrics;
22pub use interceptor::UserAgentInterceptor;
23pub use metrics::BusinessMetric;
24
25#[derive(Clone, Debug)]
31pub struct AwsUserAgent {
32 sdk_metadata: SdkMetadata,
33 ua_metadata: UaMetadata,
34 api_metadata: ApiMetadata,
35 os_metadata: OsMetadata,
36 language_metadata: LanguageMetadata,
37 exec_env_metadata: Option<ExecEnvMetadata>,
38 business_metrics: BusinessMetrics,
39 framework_metadata: Vec<FrameworkMetadata>,
40 app_name: Option<AppName>,
41 build_env_additional_metadata: Option<AdditionalMetadata>,
42 additional_metadata: Vec<AdditionalMetadata>,
43}
44
45impl AwsUserAgent {
46 pub fn new_from_environment(env: Env, api_metadata: ApiMetadata) -> Self {
52 let build_metadata = &BUILD_METADATA;
53 let sdk_metadata = SdkMetadata {
54 name: "rust",
55 version: build_metadata.core_pkg_version,
56 };
57 let ua_metadata = UaMetadata {
58 version: USER_AGENT_VERSION,
59 };
60 let os_metadata = OsMetadata {
61 os_family: &build_metadata.os_family,
62 version: None,
63 };
64 let exec_env_metadata = env
65 .get("AWS_EXECUTION_ENV")
66 .ok()
67 .map(|name| ExecEnvMetadata { name });
68
69 let build_env_additional_metadata = option_env!("AWS_SDK_RUST_BUILD_UA_METADATA")
71 .and_then(|value| AdditionalMetadata::new(value).ok());
72
73 AwsUserAgent {
74 sdk_metadata,
75 ua_metadata,
76 api_metadata,
77 os_metadata,
78 language_metadata: LanguageMetadata {
79 lang: "rust",
80 version: BUILD_METADATA.rust_version,
81 extras: Default::default(),
82 },
83 exec_env_metadata,
84 framework_metadata: Default::default(),
85 business_metrics: Default::default(),
86 app_name: Default::default(),
87 build_env_additional_metadata,
88 additional_metadata: Default::default(),
89 }
90 }
91
92 pub fn for_tests() -> Self {
96 Self {
97 sdk_metadata: SdkMetadata {
98 name: "rust",
99 version: "0.123.test",
100 },
101 ua_metadata: UaMetadata { version: "0.1" },
102 api_metadata: ApiMetadata {
103 service_id: "test-service".into(),
104 version: "0.123",
105 },
106 os_metadata: OsMetadata {
107 os_family: &OsFamily::Windows,
108 version: Some("XPSP3".to_string()),
109 },
110 language_metadata: LanguageMetadata {
111 lang: "rust",
112 version: "1.50.0",
113 extras: Default::default(),
114 },
115 exec_env_metadata: None,
116 business_metrics: Default::default(),
117 framework_metadata: Vec::new(),
118 app_name: None,
119 build_env_additional_metadata: None,
120 additional_metadata: Vec::new(),
121 }
122 }
123
124 #[deprecated(
125 since = "1.4.0",
126 note = "This is a no-op; use `with_business_metric` instead."
127 )]
128 #[allow(unused_mut)]
129 #[allow(deprecated)]
130 #[doc(hidden)]
131 pub fn with_feature_metadata(mut self, _metadata: FeatureMetadata) -> Self {
133 self
134 }
135
136 #[deprecated(
137 since = "1.4.0",
138 note = "This is a no-op; use `add_business_metric` instead."
139 )]
140 #[allow(deprecated)]
141 #[allow(unused_mut)]
142 #[doc(hidden)]
143 pub fn add_feature_metadata(&mut self, _metadata: FeatureMetadata) -> &mut Self {
145 self
146 }
147
148 #[deprecated(
149 since = "1.4.0",
150 note = "This is a no-op; use `with_business_metric` instead."
151 )]
152 #[allow(deprecated)]
153 #[allow(unused_mut)]
154 #[doc(hidden)]
155 pub fn with_config_metadata(mut self, _metadata: ConfigMetadata) -> Self {
157 self
158 }
159
160 #[deprecated(
161 since = "1.4.0",
162 note = "This is a no-op; use `add_business_metric` instead."
163 )]
164 #[allow(deprecated)]
165 #[allow(unused_mut)]
166 #[doc(hidden)]
167 pub fn add_config_metadata(&mut self, _metadata: ConfigMetadata) -> &mut Self {
169 self
170 }
171
172 #[doc(hidden)]
173 pub fn with_business_metric(mut self, metric: BusinessMetric) -> Self {
175 self.business_metrics.push(metric);
176 self
177 }
178
179 #[doc(hidden)]
180 pub fn add_business_metric(&mut self, metric: BusinessMetric) -> &mut Self {
182 self.business_metrics.push(metric);
183 self
184 }
185
186 #[doc(hidden)]
187 pub fn with_framework_metadata(mut self, metadata: FrameworkMetadata) -> Self {
189 self.framework_metadata.push(metadata);
190 self
191 }
192
193 #[doc(hidden)]
194 pub fn add_framework_metadata(&mut self, metadata: FrameworkMetadata) -> &mut Self {
196 self.framework_metadata.push(metadata);
197 self
198 }
199
200 pub fn with_additional_metadata(mut self, metadata: AdditionalMetadata) -> Self {
202 self.additional_metadata.push(metadata);
203 self
204 }
205
206 pub fn add_additional_metadata(&mut self, metadata: AdditionalMetadata) -> &mut Self {
208 self.additional_metadata.push(metadata);
209 self
210 }
211
212 pub fn with_app_name(mut self, app_name: AppName) -> Self {
214 self.app_name = Some(app_name);
215 self
216 }
217
218 pub fn set_app_name(&mut self, app_name: AppName) -> &mut Self {
220 self.app_name = Some(app_name);
221 self
222 }
223
224 pub fn aws_ua_header(&self) -> String {
228 let mut ua_value = String::new();
242 use std::fmt::Write;
243 write!(ua_value, "{} ", &self.sdk_metadata).unwrap();
245 write!(ua_value, "{} ", &self.ua_metadata).unwrap();
246 write!(ua_value, "{} ", &self.api_metadata).unwrap();
247 write!(ua_value, "{} ", &self.os_metadata).unwrap();
248 write!(ua_value, "{} ", &self.language_metadata).unwrap();
249 if let Some(ref env_meta) = self.exec_env_metadata {
250 write!(ua_value, "{env_meta} ").unwrap();
251 }
252 if !self.business_metrics.is_empty() {
253 write!(ua_value, "{} ", &self.business_metrics).unwrap()
254 }
255 for framework in &self.framework_metadata {
256 write!(ua_value, "{framework} ").unwrap();
257 }
258 for additional_metadata in &self.additional_metadata {
259 write!(ua_value, "{additional_metadata} ").unwrap();
260 }
261 if let Some(app_name) = &self.app_name {
262 write!(ua_value, "app/{app_name}").unwrap();
263 }
264 if let Some(additional_metadata) = &self.build_env_additional_metadata {
265 write!(ua_value, "{additional_metadata}").unwrap();
266 }
267 if ua_value.ends_with(' ') {
268 ua_value.truncate(ua_value.len() - 1);
269 }
270 ua_value
271 }
272
273 pub fn ua_header(&self) -> String {
277 self.aws_ua_header()
278 }
279}
280
281impl Storable for AwsUserAgent {
282 type Storer = StoreReplace<Self>;
283}
284
285#[derive(Clone, Copy, Debug)]
286struct SdkMetadata {
287 name: &'static str,
288 version: &'static str,
289}
290
291impl fmt::Display for SdkMetadata {
292 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293 write!(f, "aws-sdk-{}/{}", self.name, self.version)
294 }
295}
296
297#[derive(Clone, Copy, Debug)]
298struct UaMetadata {
299 version: &'static str,
300}
301
302impl fmt::Display for UaMetadata {
303 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304 write!(f, "ua/{}", self.version)
305 }
306}
307
308#[derive(Clone, Debug)]
310pub struct ApiMetadata {
311 service_id: Cow<'static, str>,
312 version: &'static str,
313}
314
315impl ApiMetadata {
316 pub const fn new(service_id: &'static str, version: &'static str) -> Self {
318 Self {
319 service_id: Cow::Borrowed(service_id),
320 version,
321 }
322 }
323}
324
325impl fmt::Display for ApiMetadata {
326 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327 write!(f, "api/{}/{}", self.service_id, self.version)
328 }
329}
330
331impl Storable for ApiMetadata {
332 type Storer = StoreReplace<Self>;
333}
334
335#[derive(Debug)]
343#[non_exhaustive]
344pub struct InvalidMetadataValue;
345
346impl Error for InvalidMetadataValue {}
347
348impl fmt::Display for InvalidMetadataValue {
349 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
350 write!(
351 f,
352 "User agent metadata can only have alphanumeric characters, or any of \
353 '!' | '#' | '$' | '%' | '&' | '\\'' | '*' | '+' | '-' | \
354 '.' | '^' | '_' | '`' | '|' | '~'"
355 )
356 }
357}
358
359fn validate_metadata(value: Cow<'static, str>) -> Result<Cow<'static, str>, InvalidMetadataValue> {
360 fn valid_character(c: char) -> bool {
361 match c {
362 _ if c.is_ascii_alphanumeric() => true,
363 '!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '.' | '^' | '_' | '`' | '|'
364 | '~' => true,
365 _ => false,
366 }
367 }
368 if !value.chars().all(valid_character) {
369 return Err(InvalidMetadataValue);
370 }
371 Ok(value)
372}
373
374#[doc(hidden)]
375#[derive(Clone, Debug)]
377#[non_exhaustive]
378pub struct AdditionalMetadata {
379 value: Cow<'static, str>,
380}
381
382impl AdditionalMetadata {
383 pub fn new(value: impl Into<Cow<'static, str>>) -> Result<Self, InvalidMetadataValue> {
391 Ok(Self {
392 value: validate_metadata(value.into())?,
393 })
394 }
395}
396
397impl fmt::Display for AdditionalMetadata {
398 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
399 write!(f, "md/{}", self.value)
401 }
402}
403
404#[derive(Clone, Debug, Default)]
405struct AdditionalMetadataList(Vec<AdditionalMetadata>);
406
407impl AdditionalMetadataList {
408 fn push(&mut self, metadata: AdditionalMetadata) {
409 self.0.push(metadata);
410 }
411}
412
413impl fmt::Display for AdditionalMetadataList {
414 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
415 for metadata in &self.0 {
416 write!(f, " {metadata}")?;
417 }
418 Ok(())
419 }
420}
421
422#[deprecated(since = "1.4.0", note = "Replaced by `BusinessMetric`.")]
423#[doc(hidden)]
424#[derive(Clone, Debug)]
426#[non_exhaustive]
427pub struct FeatureMetadata {
428 name: Cow<'static, str>,
429 version: Option<Cow<'static, str>>,
430 additional: AdditionalMetadataList,
431}
432
433#[allow(deprecated)]
434impl FeatureMetadata {
435 pub fn new(
443 name: impl Into<Cow<'static, str>>,
444 version: Option<Cow<'static, str>>,
445 ) -> Result<Self, InvalidMetadataValue> {
446 Ok(Self {
447 name: validate_metadata(name.into())?,
448 version: version.map(validate_metadata).transpose()?,
449 additional: Default::default(),
450 })
451 }
452
453 pub fn with_additional(mut self, metadata: AdditionalMetadata) -> Self {
455 self.additional.push(metadata);
456 self
457 }
458}
459
460#[allow(deprecated)]
461impl fmt::Display for FeatureMetadata {
462 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
463 if let Some(version) = &self.version {
465 write!(f, "ft/{}/{}{}", self.name, version, self.additional)
466 } else {
467 write!(f, "ft/{}{}", self.name, self.additional)
468 }
469 }
470}
471
472#[deprecated(since = "1.4.0", note = "Replaced by `BusinessMetric`.")]
473#[doc(hidden)]
474#[derive(Clone, Debug)]
476#[non_exhaustive]
477pub struct ConfigMetadata {
478 config: Cow<'static, str>,
479 value: Option<Cow<'static, str>>,
480}
481
482#[allow(deprecated)]
483impl ConfigMetadata {
484 pub fn new(
492 config: impl Into<Cow<'static, str>>,
493 value: Option<Cow<'static, str>>,
494 ) -> Result<Self, InvalidMetadataValue> {
495 Ok(Self {
496 config: validate_metadata(config.into())?,
497 value: value.map(validate_metadata).transpose()?,
498 })
499 }
500}
501
502#[allow(deprecated)]
503impl fmt::Display for ConfigMetadata {
504 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
505 if let Some(value) = &self.value {
507 write!(f, "cfg/{}/{}", self.config, value)
508 } else {
509 write!(f, "cfg/{}", self.config)
510 }
511 }
512}
513
514pub type FrameworkMetadata = aws_types::sdk_ua_metadata::FrameworkMetadata;
520
521#[derive(Clone, Debug)]
522struct OsMetadata {
523 os_family: &'static OsFamily,
524 version: Option<String>,
525}
526
527impl fmt::Display for OsMetadata {
528 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
529 let os_family = match self.os_family {
530 OsFamily::Windows => "windows",
531 OsFamily::Linux => "linux",
532 OsFamily::Macos => "macos",
533 OsFamily::Android => "android",
534 OsFamily::Ios => "ios",
535 OsFamily::Other => "other",
536 };
537 write!(f, "os/{os_family}")?;
538 if let Some(ref version) = self.version {
539 write!(f, "/{version}")?;
540 }
541 Ok(())
542 }
543}
544
545#[derive(Clone, Debug)]
546struct LanguageMetadata {
547 lang: &'static str,
548 version: &'static str,
549 extras: AdditionalMetadataList,
550}
551impl fmt::Display for LanguageMetadata {
552 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
553 write!(f, "lang/{}/{}{}", self.lang, self.version, self.extras)
555 }
556}
557
558#[derive(Clone, Debug)]
559struct ExecEnvMetadata {
560 name: String,
561}
562impl fmt::Display for ExecEnvMetadata {
563 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
564 write!(f, "exec-env/{}", &self.name)
565 }
566}
567
568#[cfg(test)]
569mod test {
570 use super::*;
571 use aws_types::app_name::AppName;
572 use aws_types::build_metadata::OsFamily;
573 use aws_types::os_shim_internal::Env;
574
575 fn make_deterministic(ua: &mut AwsUserAgent) {
576 ua.sdk_metadata.version = "0.1";
578 ua.ua_metadata.version = "0.1";
579 ua.language_metadata.version = "1.50.0";
580 ua.os_metadata.os_family = &OsFamily::Macos;
581 ua.os_metadata.version = Some("1.15".to_string());
582 }
583
584 #[test]
585 fn generate_a_valid_ua() {
586 let api_metadata = ApiMetadata {
587 service_id: "dynamodb".into(),
588 version: "123",
589 };
590 let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata);
591 make_deterministic(&mut ua);
592 assert_eq!(
593 ua.aws_ua_header(),
594 "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0"
595 );
596 assert_eq!(
597 ua.ua_header(),
598 "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0"
599 );
600 }
601
602 #[test]
603 fn generate_a_valid_ua_with_execution_env() {
604 let api_metadata = ApiMetadata {
605 service_id: "dynamodb".into(),
606 version: "123",
607 };
608 let mut ua = AwsUserAgent::new_from_environment(
609 Env::from_slice(&[("AWS_EXECUTION_ENV", "lambda")]),
610 api_metadata,
611 );
612 make_deterministic(&mut ua);
613 assert_eq!(
614 ua.aws_ua_header(),
615 "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 exec-env/lambda"
616 );
617 assert_eq!(
618 ua.ua_header(),
619 "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 exec-env/lambda"
620 );
621 }
622
623 #[test]
624 fn generate_a_valid_ua_with_frameworks() {
625 let api_metadata = ApiMetadata {
626 service_id: "dynamodb".into(),
627 version: "123",
628 };
629 let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata)
630 .with_framework_metadata(FrameworkMetadata::new("some-framework", Some("1.3")).unwrap())
631 .with_framework_metadata(FrameworkMetadata::new("other", None::<&str>).unwrap());
632 make_deterministic(&mut ua);
633 assert_eq!(
634 ua.aws_ua_header(),
635 "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 lib/some-framework/1.3 lib/other"
636 );
637 assert_eq!(
638 ua.ua_header(),
639 "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 lib/some-framework/1.3 lib/other"
640 );
641 }
642
643 #[test]
644 fn generate_a_valid_ua_with_app_name() {
645 let api_metadata = ApiMetadata {
646 service_id: "dynamodb".into(),
647 version: "123",
648 };
649 let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata)
650 .with_app_name(AppName::new("my_app").unwrap());
651 make_deterministic(&mut ua);
652 assert_eq!(
653 ua.aws_ua_header(),
654 "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 app/my_app"
655 );
656 assert_eq!(
657 ua.ua_header(),
658 "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 app/my_app"
659 );
660 }
661
662 #[test]
663 fn generate_a_valid_ua_with_build_env_additional_metadata() {
664 let mut ua = AwsUserAgent::for_tests();
665 ua.build_env_additional_metadata = Some(AdditionalMetadata::new("asdf").unwrap());
666 assert_eq!(
667 ua.aws_ua_header(),
668 "aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 md/asdf"
669 );
670 assert_eq!(
671 ua.ua_header(),
672 "aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 md/asdf"
673 );
674 }
675
676 #[test]
677 fn generate_a_valid_ua_with_business_metrics() {
678 {
680 let ua = AwsUserAgent::for_tests().with_business_metric(BusinessMetric::ResourceModel);
681 assert_eq!(
682 ua.aws_ua_header(),
683 "aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 m/A"
684 );
685 assert_eq!(
686 ua.ua_header(),
687 "aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 m/A"
688 );
689 }
690 {
692 let ua = AwsUserAgent::for_tests()
693 .with_business_metric(BusinessMetric::RetryModeAdaptive)
694 .with_business_metric(BusinessMetric::S3Transfer)
695 .with_business_metric(BusinessMetric::S3ExpressBucket);
696 assert_eq!(
697 ua.aws_ua_header(),
698 "aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 m/F,G,J"
699 );
700 assert_eq!(
701 ua.ua_header(),
702 "aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 m/F,G,J"
703 );
704 }
705 }
706}
707
708