From d0792f31ca534624988b91b07d9878d2b2ba8b18 Mon Sep 17 00:00:00 2001 From: Lalit Kumar Bhasin Date: Wed, 30 Jul 2025 15:31:04 -0700 Subject: [PATCH 01/21] fix --- .../geneva-uploader/src/client.rs | 4 ++++ .../src/config_service/client.rs | 21 +++++++++++++++---- .../geneva-uploader/src/config_service/mod.rs | 6 ++++++ .../src/ingestion_service/mod.rs | 1 + .../examples/basic.rs | 1 + 5 files changed, 29 insertions(+), 4 deletions(-) diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs index 72bf5dd42..50705a470 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs @@ -23,6 +23,9 @@ pub struct GenevaClientConfig { pub role_instance: String, /// Maximum number of concurrent uploads. If None, defaults to number of CPU cores. pub max_concurrent_uploads: Option, + /// User agent suffix for the client. Will be formatted as "RustGenevaClient-". + /// If None, defaults to "RustGenevaClient". + pub user_agent_suffix: Option, // Add event name/version here if constant, or per-upload if you want them per call. } @@ -47,6 +50,7 @@ impl GenevaClient { region: cfg.region, config_major_version: cfg.config_major_version, auth_method: cfg.auth_method, + user_agent_suffix: cfg.user_agent_suffix, }; let config_client = Arc::new( GenevaConfigClient::new(config_client_config) diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs index 123b814af..b7dd55adc 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs @@ -128,7 +128,10 @@ pub(crate) struct GenevaConfigClientConfig { pub(crate) namespace: String, pub(crate) region: String, pub(crate) config_major_version: u32, - pub(crate) auth_method: AuthMethod, // agent_identity and agent_version are hardcoded for now + pub(crate) auth_method: AuthMethod, + /// User agent suffix for the client. Will be formatted as "RustGenevaClient-". + /// If None, defaults to "RustGenevaClient". + pub(crate) user_agent_suffix: Option, } #[allow(dead_code)] @@ -261,7 +264,9 @@ impl GenevaConfigClient { let agent_identity = "GenevaUploader"; let agent_version = "0.1"; - let static_headers = Self::build_static_headers(agent_identity, agent_version); + let user_agent_suffix = config.user_agent_suffix.as_deref().unwrap_or(""); + let static_headers = + Self::build_static_headers(agent_identity, agent_version, user_agent_suffix); let identity = format!("Tenant=Default/Role=GcsClient/RoleInstance={agent_identity}"); @@ -302,9 +307,17 @@ impl GenevaConfigClient { .map(|dt| dt.with_timezone(&Utc)) } - fn build_static_headers(agent_identity: &str, agent_version: &str) -> HeaderMap { + fn build_static_headers( + _agent_identity: &str, + _agent_version: &str, + user_agent_suffix: &str, + ) -> HeaderMap { let mut headers = HeaderMap::new(); - let user_agent = format!("{agent_identity}-{agent_version}"); + let user_agent = if user_agent_suffix.is_empty() { + "RustGenevaClient".to_string() + } else { + format!("RustGenevaClient-{}", user_agent_suffix) + }; headers.insert(USER_AGENT, HeaderValue::from_str(&user_agent).unwrap()); headers.insert(ACCEPT, HeaderValue::from_static("application/json")); headers diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/mod.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/mod.rs index c41ecfa2d..32c405c1c 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/mod.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/mod.rs @@ -21,6 +21,7 @@ mod tests { region: "region".to_string(), config_major_version: 1, auth_method: AuthMethod::ManagedIdentity, + user_agent_suffix: Some("TestConfig".to_string()), }; assert_eq!(config.environment, "env"); @@ -107,6 +108,7 @@ mod tests { path: PathBuf::from(temp_p12_file.path().to_string_lossy().to_string()), password, }, + user_agent_suffix: Some("MockedTest".to_string()), }; let client = GenevaConfigClient::new(config).unwrap(); @@ -152,6 +154,7 @@ mod tests { path: PathBuf::from(temp_p12_file.path().to_string_lossy().to_string()), password, }, + user_agent_suffix: Some("ErrorHandlingTest".to_string()), }; let client = GenevaConfigClient::new(config).unwrap(); @@ -200,6 +203,7 @@ mod tests { path: PathBuf::from(temp_p12_file.path().to_string_lossy().to_string()), password, }, + user_agent_suffix: Some("MissingInfoTest".to_string()), }; let client = GenevaConfigClient::new(config).unwrap(); @@ -231,6 +235,7 @@ mod tests { path: PathBuf::from("/nonexistent/path.p12".to_string()), password: "test".to_string(), }, + user_agent_suffix: Some("InvalidCertTest".to_string()), }; let result = GenevaConfigClient::new(config); @@ -294,6 +299,7 @@ mod tests { path: PathBuf::from(cert_path), password: cert_password, }, + user_agent_suffix: Some("RealServerTest".to_string()), }; println!("Connecting to real Geneva Config service..."); diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/mod.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/mod.rs index 55eed1c9d..ee19c7b7b 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/mod.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/mod.rs @@ -64,6 +64,7 @@ mod tests { path: cert_path, password: cert_password, }, + user_agent_suffix: Some("TestUploader".to_string()), }; // Build client and uploader diff --git a/opentelemetry-exporter-geneva/opentelemetry-exporter-geneva/examples/basic.rs b/opentelemetry-exporter-geneva/opentelemetry-exporter-geneva/examples/basic.rs index e655212a9..b0788dc62 100644 --- a/opentelemetry-exporter-geneva/opentelemetry-exporter-geneva/examples/basic.rs +++ b/opentelemetry-exporter-geneva/opentelemetry-exporter-geneva/examples/basic.rs @@ -63,6 +63,7 @@ async fn main() { role_name, role_instance, max_concurrent_uploads: None, // Use default + user_agent_suffix: Some("BasicExample".to_string()), }; let geneva_client = GenevaClient::new(config) From 880d6e510ec9a90cfffda20faf497c59d2768284 Mon Sep 17 00:00:00 2001 From: Lalit Kumar Bhasin Date: Wed, 30 Jul 2025 15:41:26 -0700 Subject: [PATCH 02/21] fix --- .../geneva-uploader/src/client.rs | 4 ++-- .../geneva-uploader/src/config_service/client.rs | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs index 50705a470..2e9af1869 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs @@ -23,8 +23,8 @@ pub struct GenevaClientConfig { pub role_instance: String, /// Maximum number of concurrent uploads. If None, defaults to number of CPU cores. pub max_concurrent_uploads: Option, - /// User agent suffix for the client. Will be formatted as "RustGenevaClient-". - /// If None, defaults to "RustGenevaClient". + /// User agent suffix for the client. Will be formatted as "RustGenevaClient--0.1". + /// If None, defaults to "RustGenevaClient-0.1". pub user_agent_suffix: Option, // Add event name/version here if constant, or per-upload if you want them per call. } diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs index b7dd55adc..facc576ac 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs @@ -129,8 +129,8 @@ pub(crate) struct GenevaConfigClientConfig { pub(crate) region: String, pub(crate) config_major_version: u32, pub(crate) auth_method: AuthMethod, - /// User agent suffix for the client. Will be formatted as "RustGenevaClient-". - /// If None, defaults to "RustGenevaClient". + /// User agent suffix for the client. Will be formatted as "RustGenevaClient--0.1". + /// If None, defaults to "RustGenevaClient-0.1". pub(crate) user_agent_suffix: Option, } @@ -262,7 +262,7 @@ impl GenevaConfigClient { } } - let agent_identity = "GenevaUploader"; + let agent_identity = "RustGenevaClient"; let agent_version = "0.1"; let user_agent_suffix = config.user_agent_suffix.as_deref().unwrap_or(""); let static_headers = @@ -308,15 +308,15 @@ impl GenevaConfigClient { } fn build_static_headers( - _agent_identity: &str, - _agent_version: &str, + agent_identity: &str, + agent_version: &str, user_agent_suffix: &str, ) -> HeaderMap { let mut headers = HeaderMap::new(); let user_agent = if user_agent_suffix.is_empty() { - "RustGenevaClient".to_string() + format!("{}-{}", agent_identity, agent_version) } else { - format!("RustGenevaClient-{}", user_agent_suffix) + format!("{}-{}-{}", agent_identity, user_agent_suffix, agent_version) }; headers.insert(USER_AGENT, HeaderValue::from_str(&user_agent).unwrap()); headers.insert(ACCEPT, HeaderValue::from_static("application/json")); From 0a60b825c8dc1da3f55a87cc7ab8af4b0d869d85 Mon Sep 17 00:00:00 2001 From: Lalit Kumar Bhasin Date: Wed, 30 Jul 2025 15:47:57 -0700 Subject: [PATCH 03/21] fix --- .../geneva-uploader/src/client.rs | 2 +- .../geneva-uploader/src/config_service/client.rs | 2 +- .../geneva-uploader/src/config_service/mod.rs | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs index 2e9af1869..fe80f0942 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs @@ -25,7 +25,7 @@ pub struct GenevaClientConfig { pub max_concurrent_uploads: Option, /// User agent suffix for the client. Will be formatted as "RustGenevaClient--0.1". /// If None, defaults to "RustGenevaClient-0.1". - pub user_agent_suffix: Option, + pub user_agent_suffix: Option<&'static str>, // Add event name/version here if constant, or per-upload if you want them per call. } diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs index facc576ac..5bc6b2e59 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs @@ -131,7 +131,7 @@ pub(crate) struct GenevaConfigClientConfig { pub(crate) auth_method: AuthMethod, /// User agent suffix for the client. Will be formatted as "RustGenevaClient--0.1". /// If None, defaults to "RustGenevaClient-0.1". - pub(crate) user_agent_suffix: Option, + pub(crate) user_agent_suffix: Option<&'static str>, } #[allow(dead_code)] diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/mod.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/mod.rs index 32c405c1c..dba4074ff 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/mod.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/mod.rs @@ -21,7 +21,7 @@ mod tests { region: "region".to_string(), config_major_version: 1, auth_method: AuthMethod::ManagedIdentity, - user_agent_suffix: Some("TestConfig".to_string()), + user_agent_suffix: Some("TestConfig"), }; assert_eq!(config.environment, "env"); @@ -108,7 +108,7 @@ mod tests { path: PathBuf::from(temp_p12_file.path().to_string_lossy().to_string()), password, }, - user_agent_suffix: Some("MockedTest".to_string()), + user_agent_suffix: Some("MockedTest"), }; let client = GenevaConfigClient::new(config).unwrap(); @@ -154,7 +154,7 @@ mod tests { path: PathBuf::from(temp_p12_file.path().to_string_lossy().to_string()), password, }, - user_agent_suffix: Some("ErrorHandlingTest".to_string()), + user_agent_suffix: Some("ErrorHandlingTest"), }; let client = GenevaConfigClient::new(config).unwrap(); @@ -203,7 +203,7 @@ mod tests { path: PathBuf::from(temp_p12_file.path().to_string_lossy().to_string()), password, }, - user_agent_suffix: Some("MissingInfoTest".to_string()), + user_agent_suffix: Some("MissingInfoTest"), }; let client = GenevaConfigClient::new(config).unwrap(); @@ -235,7 +235,7 @@ mod tests { path: PathBuf::from("/nonexistent/path.p12".to_string()), password: "test".to_string(), }, - user_agent_suffix: Some("InvalidCertTest".to_string()), + user_agent_suffix: Some("InvalidCertTest"), }; let result = GenevaConfigClient::new(config); @@ -299,7 +299,7 @@ mod tests { path: PathBuf::from(cert_path), password: cert_password, }, - user_agent_suffix: Some("RealServerTest".to_string()), + user_agent_suffix: Some("RealServerTest"), }; println!("Connecting to real Geneva Config service..."); From 78be687e9cd203ce33e7b97d8d1fc3c1f6bbdb55 Mon Sep 17 00:00:00 2001 From: Lalit Kumar Bhasin Date: Wed, 30 Jul 2025 15:57:42 -0700 Subject: [PATCH 04/21] more changes --- .../geneva-uploader/src/client.rs | 9 +++++++-- .../geneva-uploader/src/config_service/client.rs | 13 +++++++++---- .../geneva-uploader/src/config_service/mod.rs | 12 ++++++------ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs index fe80f0942..a2b294599 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs @@ -23,8 +23,13 @@ pub struct GenevaClientConfig { pub role_instance: String, /// Maximum number of concurrent uploads. If None, defaults to number of CPU cores. pub max_concurrent_uploads: Option, - /// User agent suffix for the client. Will be formatted as "RustGenevaClient--0.1". - /// If None, defaults to "RustGenevaClient-0.1". + /// User agent for the application. Will be formatted as " (RustGenevaClient/0.1)". + /// If None, defaults to "RustGenevaClient/0.1". + /// + /// Examples: + /// - None: "RustGenevaClient/0.1" + /// - Some("MyApp/2.1.0"): "MyApp/2.1.0 (RustGenevaClient/0.1)" + /// - Some("ProductionService-1.0"): "ProductionService-1.0 (RustGenevaClient/0.1)" pub user_agent_suffix: Option<&'static str>, // Add event name/version here if constant, or per-upload if you want them per call. } diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs index 5bc6b2e59..2ec1fc5b0 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs @@ -129,8 +129,13 @@ pub(crate) struct GenevaConfigClientConfig { pub(crate) region: String, pub(crate) config_major_version: u32, pub(crate) auth_method: AuthMethod, - /// User agent suffix for the client. Will be formatted as "RustGenevaClient--0.1". - /// If None, defaults to "RustGenevaClient-0.1". + /// User agent for the application. Will be formatted as " (RustGenevaClient/0.1)". + /// If None, defaults to "RustGenevaClient/0.1". + /// + /// Examples: + /// - None: "RustGenevaClient/0.1" + /// - Some("MyApp/2.1.0"): "MyApp/2.1.0 (RustGenevaClient/0.1)" + /// - Some("ProductionService-1.0"): "ProductionService-1.0 (RustGenevaClient/0.1)" pub(crate) user_agent_suffix: Option<&'static str>, } @@ -314,9 +319,9 @@ impl GenevaConfigClient { ) -> HeaderMap { let mut headers = HeaderMap::new(); let user_agent = if user_agent_suffix.is_empty() { - format!("{}-{}", agent_identity, agent_version) + format!("{}/{}", agent_identity, agent_version) } else { - format!("{}-{}-{}", agent_identity, user_agent_suffix, agent_version) + format!("{} ({}/{})", user_agent_suffix, agent_identity, agent_version) }; headers.insert(USER_AGENT, HeaderValue::from_str(&user_agent).unwrap()); headers.insert(ACCEPT, HeaderValue::from_static("application/json")); diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/mod.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/mod.rs index dba4074ff..3c01545dd 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/mod.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/mod.rs @@ -21,7 +21,7 @@ mod tests { region: "region".to_string(), config_major_version: 1, auth_method: AuthMethod::ManagedIdentity, - user_agent_suffix: Some("TestConfig"), + user_agent_suffix: Some("TestApp/1.0"), }; assert_eq!(config.environment, "env"); @@ -108,7 +108,7 @@ mod tests { path: PathBuf::from(temp_p12_file.path().to_string_lossy().to_string()), password, }, - user_agent_suffix: Some("MockedTest"), + user_agent_suffix: Some("MockClient/1.0"), }; let client = GenevaConfigClient::new(config).unwrap(); @@ -154,7 +154,7 @@ mod tests { path: PathBuf::from(temp_p12_file.path().to_string_lossy().to_string()), password, }, - user_agent_suffix: Some("ErrorHandlingTest"), + user_agent_suffix: Some("ErrorTestApp/1.0"), }; let client = GenevaConfigClient::new(config).unwrap(); @@ -203,7 +203,7 @@ mod tests { path: PathBuf::from(temp_p12_file.path().to_string_lossy().to_string()), password, }, - user_agent_suffix: Some("MissingInfoTest"), + user_agent_suffix: Some("MissingInfoTestApp/1.0"), }; let client = GenevaConfigClient::new(config).unwrap(); @@ -235,7 +235,7 @@ mod tests { path: PathBuf::from("/nonexistent/path.p12".to_string()), password: "test".to_string(), }, - user_agent_suffix: Some("InvalidCertTest"), + user_agent_suffix: Some("InvalidCertTestApp/1.0"), }; let result = GenevaConfigClient::new(config); @@ -299,7 +299,7 @@ mod tests { path: PathBuf::from(cert_path), password: cert_password, }, - user_agent_suffix: Some("RealServerTest"), + user_agent_suffix: Some("RealServerTestApp/1.0"), }; println!("Connecting to real Geneva Config service..."); From 99f10d77338c59633b107ac925c161b33334ad49 Mon Sep 17 00:00:00 2001 From: Lalit Kumar Bhasin Date: Fri, 1 Aug 2025 09:28:57 -0700 Subject: [PATCH 05/21] fix --- .../geneva-uploader/src/client.rs | 5 +- .../src/config_service/client.rs | 197 +++++++++++++++++- .../src/ingestion_service/mod.rs | 2 +- 3 files changed, 196 insertions(+), 8 deletions(-) diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs index a2b294599..d994a0690 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs @@ -25,7 +25,10 @@ pub struct GenevaClientConfig { pub max_concurrent_uploads: Option, /// User agent for the application. Will be formatted as " (RustGenevaClient/0.1)". /// If None, defaults to "RustGenevaClient/0.1". - /// + /// + /// The suffix must contain only ASCII printable characters, be non-empty (after trimming), + /// and not exceed 200 characters in length. + /// /// Examples: /// - None: "RustGenevaClient/0.1" /// - Some("MyApp/2.1.0"): "MyApp/2.1.0 (RustGenevaClient/0.1)" diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs index 2ec1fc5b0..dceb560a2 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs @@ -83,6 +83,10 @@ pub(crate) enum GenevaConfigClientError { #[error("JSON error: {0}")] SerdeJson(#[from] serde_json::Error), + // Validation + #[error("Invalid user agent suffix: {0}")] + InvalidUserAgentSuffix(String), + // Misc #[error("Moniker not found: {0}")] MonikerNotFound(String), @@ -131,7 +135,10 @@ pub(crate) struct GenevaConfigClientConfig { pub(crate) auth_method: AuthMethod, /// User agent for the application. Will be formatted as " (RustGenevaClient/0.1)". /// If None, defaults to "RustGenevaClient/0.1". - /// + /// + /// The suffix must contain only ASCII printable characters, be non-empty (after trimming), + /// and not exceed 200 characters in length. + /// /// Examples: /// - None: "RustGenevaClient/0.1" /// - Some("MyApp/2.1.0"): "MyApp/2.1.0 (RustGenevaClient/0.1)" @@ -215,6 +222,58 @@ impl fmt::Debug for GenevaConfigClient { } } +/// Validates a user agent suffix for HTTP header compliance +/// +/// # Arguments +/// * `suffix` - The user agent suffix to validate +/// +/// # Returns +/// * `Ok(())` if valid +/// * `Err(GenevaConfigClientError::InvalidUserAgentSuffix)` if invalid +/// +/// # Validation Rules +/// - Must contain only ASCII printable characters (0x20-0x7E) +/// - Must not contain control characters (especially \r, \n, \0) +/// - Must not exceed 200 characters in length +/// - Must not be empty or only whitespace +fn validate_user_agent_suffix(suffix: &str) -> Result<()> { + if suffix.trim().is_empty() { + return Err(GenevaConfigClientError::InvalidUserAgentSuffix( + "User agent suffix cannot be empty or only whitespace".to_string(), + )); + } + + if suffix.len() > 200 { + return Err(GenevaConfigClientError::InvalidUserAgentSuffix(format!( + "User agent suffix too long: {} characters (max 200)", + suffix.len() + ))); + } + + // Check for invalid characters + for (i, ch) in suffix.char_indices() { + match ch { + // Control characters that would break HTTP headers + '\r' | '\n' | '\0' => { + return Err(GenevaConfigClientError::InvalidUserAgentSuffix(format!( + "Invalid control character at position {}: {:?}", + i, ch + ))); + } + // Non-ASCII or non-printable characters + ch if !ch.is_ascii() || (ch as u8) < 0x20 || (ch as u8) > 0x7E => { + return Err(GenevaConfigClientError::InvalidUserAgentSuffix(format!( + "Invalid character at position {}: {:?} (must be ASCII printable)", + i, ch + ))); + } + _ => {} // Valid character + } + } + + Ok(()) +} + /// Client for interacting with the Geneva Configuration Service. /// /// This client handles authentication and communication with the Geneva Config @@ -233,6 +292,11 @@ impl GenevaConfigClient { /// * `GenevaConfigClientError::AuthMethodNotImplemented` - If the specified authentication method is not yet supported #[allow(dead_code)] pub(crate) fn new(config: GenevaConfigClientConfig) -> Result { + // Validate user_agent_suffix if provided + if let Some(suffix) = config.user_agent_suffix { + validate_user_agent_suffix(suffix)?; + } + let mut client_builder = Client::builder() .http1_only() .timeout(Duration::from_secs(30)); //TODO - make this configurable @@ -271,7 +335,7 @@ impl GenevaConfigClient { let agent_version = "0.1"; let user_agent_suffix = config.user_agent_suffix.as_deref().unwrap_or(""); let static_headers = - Self::build_static_headers(agent_identity, agent_version, user_agent_suffix); + Self::build_static_headers(agent_identity, agent_version, user_agent_suffix)?; let identity = format!("Tenant=Default/Role=GcsClient/RoleInstance={agent_identity}"); @@ -316,16 +380,28 @@ impl GenevaConfigClient { agent_identity: &str, agent_version: &str, user_agent_suffix: &str, - ) -> HeaderMap { + ) -> Result { let mut headers = HeaderMap::new(); let user_agent = if user_agent_suffix.is_empty() { format!("{}/{}", agent_identity, agent_version) } else { - format!("{} ({}/{})", user_agent_suffix, agent_identity, agent_version) + format!( + "{} ({}/{})", + user_agent_suffix, agent_identity, agent_version + ) }; - headers.insert(USER_AGENT, HeaderValue::from_str(&user_agent).unwrap()); + + // Safe header construction with proper error handling + let header_value = HeaderValue::from_str(&user_agent).map_err(|e| { + GenevaConfigClientError::InvalidUserAgentSuffix(format!( + "Failed to create User-Agent header: {}", + e + )) + })?; + + headers.insert(USER_AGENT, header_value); headers.insert(ACCEPT, HeaderValue::from_static("application/json")); - headers + Ok(headers) } /// Retrieves ingestion gateway information from the Geneva Config Service. @@ -576,3 +652,112 @@ fn configure_tls_connector( .max_protocol_version(Some(Protocol::Tlsv12)); builder } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_validate_user_agent_suffix_valid() { + assert!(validate_user_agent_suffix("MyApp/1.0").is_ok()); + assert!(validate_user_agent_suffix("Production-Service-2.1.0").is_ok()); + assert!(validate_user_agent_suffix("TestApp_v3").is_ok()); + assert!(validate_user_agent_suffix("App-Name.1.2.3").is_ok()); + assert!(validate_user_agent_suffix("Simple123").is_ok()); + } + + #[test] + fn test_validate_user_agent_suffix_empty() { + assert!(validate_user_agent_suffix("").is_err()); + assert!(validate_user_agent_suffix(" ").is_err()); + assert!(validate_user_agent_suffix("\t\n").is_err()); + + if let Err(e) = validate_user_agent_suffix("") { + assert!(e.to_string().contains("cannot be empty")); + } + } + + #[test] + fn test_validate_user_agent_suffix_too_long() { + let long_suffix = "a".repeat(201); + let result = validate_user_agent_suffix(&long_suffix); + assert!(result.is_err()); + + if let Err(e) = result { + assert!(e.to_string().contains("too long")); + assert!(e.to_string().contains("201 characters")); + } + + // Test exactly at the limit should be OK + let max_length_suffix = "a".repeat(200); + assert!(validate_user_agent_suffix(&max_length_suffix).is_ok()); + } + + #[test] + fn test_validate_user_agent_suffix_invalid_chars() { + // Test control characters + assert!(validate_user_agent_suffix("App\nName").is_err()); + assert!(validate_user_agent_suffix("App\rName").is_err()); + assert!(validate_user_agent_suffix("App\0Name").is_err()); + assert!(validate_user_agent_suffix("App\tName").is_err()); + + // Test non-ASCII characters + assert!(validate_user_agent_suffix("App🚀Name").is_err()); + assert!(validate_user_agent_suffix("Appé").is_err()); + assert!(validate_user_agent_suffix("App中文").is_err()); + + // Test non-printable ASCII + assert!(validate_user_agent_suffix("App\x1FName").is_err()); // Unit separator + assert!(validate_user_agent_suffix("App\x7FName").is_err()); // DEL character + + // Verify error messages contain position information + if let Err(e) = validate_user_agent_suffix("App\nName") { + assert!(e.to_string().contains("position 3")); + assert!(e.to_string().contains("control character")); + } + } + + #[test] + fn test_build_static_headers_safe() { + let headers = GenevaConfigClient::build_static_headers("TestAgent", "1.0", "ValidApp/2.0"); + assert!(headers.is_ok()); + + let headers = headers.unwrap(); + let user_agent = headers.get(USER_AGENT).unwrap().to_str().unwrap(); + assert_eq!(user_agent, "ValidApp/2.0 (TestAgent/1.0)"); + + // Test empty suffix + let headers = GenevaConfigClient::build_static_headers("TestAgent", "1.0", ""); + assert!(headers.is_ok()); + + let headers = headers.unwrap(); + let user_agent = headers.get(USER_AGENT).unwrap().to_str().unwrap(); + assert_eq!(user_agent, "TestAgent/1.0"); + } + + #[test] + fn test_build_static_headers_invalid() { + // This should not happen in practice due to validation, but test the safety mechanism + let result = + GenevaConfigClient::build_static_headers("TestAgent", "1.0", "Invalid\nSuffix"); + assert!(result.is_err()); + + if let Err(e) = result { + assert!(e.to_string().contains("Failed to create User-Agent header")); + } + } + + #[test] + fn test_character_validation_edge_cases() { + // Test ASCII printable range boundaries + assert!(validate_user_agent_suffix(" ").is_err()); // Space only should be trimmed to empty + assert!(validate_user_agent_suffix("App Space").is_ok()); // Space in middle is OK + assert!(validate_user_agent_suffix("~").is_ok()); // Last printable ASCII (0x7E) + assert!(validate_user_agent_suffix("!").is_ok()); // First printable ASCII after space (0x21) + + // Test that spaces at the beginning and end are allowed (they're ASCII printable) + assert!(validate_user_agent_suffix(" ValidApp ").is_ok()); // Leading/trailing spaces are valid ASCII printable chars + // But strings that trim to empty should fail + assert!(validate_user_agent_suffix(" ").is_err()); // Only spaces should fail + } +} diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/mod.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/mod.rs index ee19c7b7b..1d4949509 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/mod.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/mod.rs @@ -64,7 +64,7 @@ mod tests { path: cert_path, password: cert_password, }, - user_agent_suffix: Some("TestUploader".to_string()), + user_agent_suffix: Some("TestUploader"), }; // Build client and uploader From 76da83c6082923b7c2b8407b80c95b1883a67c5a Mon Sep 17 00:00:00 2001 From: Lalit Kumar Bhasin Date: Fri, 1 Aug 2025 09:35:39 -0700 Subject: [PATCH 06/21] fix --- .../src/config_service/client.rs | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs index dceb560a2..327504e12 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs @@ -245,8 +245,8 @@ fn validate_user_agent_suffix(suffix: &str) -> Result<()> { if suffix.len() > 200 { return Err(GenevaConfigClientError::InvalidUserAgentSuffix(format!( - "User agent suffix too long: {} characters (max 200)", - suffix.len() + "User agent suffix too long: {len} characters (max 200)", + len = suffix.len() ))); } @@ -256,15 +256,13 @@ fn validate_user_agent_suffix(suffix: &str) -> Result<()> { // Control characters that would break HTTP headers '\r' | '\n' | '\0' => { return Err(GenevaConfigClientError::InvalidUserAgentSuffix(format!( - "Invalid control character at position {}: {:?}", - i, ch + "Invalid control character at position {i}: {ch:?}" ))); } // Non-ASCII or non-printable characters ch if !ch.is_ascii() || (ch as u8) < 0x20 || (ch as u8) > 0x7E => { return Err(GenevaConfigClientError::InvalidUserAgentSuffix(format!( - "Invalid character at position {}: {:?} (must be ASCII printable)", - i, ch + "Invalid character at position {i}: {ch:?} (must be ASCII printable)" ))); } _ => {} // Valid character @@ -333,7 +331,7 @@ impl GenevaConfigClient { let agent_identity = "RustGenevaClient"; let agent_version = "0.1"; - let user_agent_suffix = config.user_agent_suffix.as_deref().unwrap_or(""); + let user_agent_suffix = config.user_agent_suffix.unwrap_or(""); let static_headers = Self::build_static_headers(agent_identity, agent_version, user_agent_suffix)?; @@ -383,19 +381,15 @@ impl GenevaConfigClient { ) -> Result { let mut headers = HeaderMap::new(); let user_agent = if user_agent_suffix.is_empty() { - format!("{}/{}", agent_identity, agent_version) + format!("{agent_identity}/{agent_version}") } else { - format!( - "{} ({}/{})", - user_agent_suffix, agent_identity, agent_version - ) + format!("{user_agent_suffix} ({agent_identity}/{agent_version})") }; // Safe header construction with proper error handling let header_value = HeaderValue::from_str(&user_agent).map_err(|e| { GenevaConfigClientError::InvalidUserAgentSuffix(format!( - "Failed to create User-Agent header: {}", - e + "Failed to create User-Agent header: {e}" )) })?; From 50d94cd7c79a3acc87c1663e8ae24aa083040b2b Mon Sep 17 00:00:00 2001 From: Lalit Kumar Bhasin Date: Fri, 1 Aug 2025 09:58:38 -0700 Subject: [PATCH 07/21] fix --- stress/src/geneva_exporter.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/stress/src/geneva_exporter.rs b/stress/src/geneva_exporter.rs index 691f077ed..a03fc7f59 100644 --- a/stress/src/geneva_exporter.rs +++ b/stress/src/geneva_exporter.rs @@ -124,6 +124,7 @@ async fn init_client() -> Result<(GenevaClient, Option), Box Result<(GenevaClient, Option), Box Result<(GenevaClient, Option), Box Date: Fri, 1 Aug 2025 10:38:02 -0700 Subject: [PATCH 08/21] fix --- .../src/config_service/client.rs | 21 ++++++++++--------- .../examples/basic.rs | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs index 327504e12..27ef6b757 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs @@ -133,16 +133,16 @@ pub(crate) struct GenevaConfigClientConfig { pub(crate) region: String, pub(crate) config_major_version: u32, pub(crate) auth_method: AuthMethod, - /// User agent for the application. Will be formatted as " (RustGenevaClient/0.1)". - /// If None, defaults to "RustGenevaClient/0.1". + /// User agent for the application. Will be formatted as " (GenevaUploader/0.1)". + /// If None, defaults to "GenevaUploader/0.1". /// /// The suffix must contain only ASCII printable characters, be non-empty (after trimming), /// and not exceed 200 characters in length. /// /// Examples: - /// - None: "RustGenevaClient/0.1" - /// - Some("MyApp/2.1.0"): "MyApp/2.1.0 (RustGenevaClient/0.1)" - /// - Some("ProductionService-1.0"): "ProductionService-1.0 (RustGenevaClient/0.1)" + /// - None: "GenevaUploader/0.1" + /// - Some("MyApp/2.1.0"): "MyApp/2.1.0 (GenevaUploader/0.1)" + /// - Some("ProductionService-1.0"): "ProductionService-1.0 (GenevaUploader/0.1)" pub(crate) user_agent_suffix: Option<&'static str>, } @@ -329,7 +329,7 @@ impl GenevaConfigClient { } } - let agent_identity = "RustGenevaClient"; + let agent_identity = "GenevaUploader"; let agent_version = "0.1"; let user_agent_suffix = config.user_agent_suffix.unwrap_or(""); let static_headers = @@ -713,20 +713,21 @@ mod tests { #[test] fn test_build_static_headers_safe() { - let headers = GenevaConfigClient::build_static_headers("TestAgent", "1.0", "ValidApp/2.0"); + let headers = + GenevaConfigClient::build_static_headers("GenevaUploader", "0.1", "ValidApp/2.0"); assert!(headers.is_ok()); let headers = headers.unwrap(); let user_agent = headers.get(USER_AGENT).unwrap().to_str().unwrap(); - assert_eq!(user_agent, "ValidApp/2.0 (TestAgent/1.0)"); + assert_eq!(user_agent, "ValidApp/2.0 (GenevaUploader/0.1)"); // Test empty suffix - let headers = GenevaConfigClient::build_static_headers("TestAgent", "1.0", ""); + let headers = GenevaConfigClient::build_static_headers("GenevaUploader", "0.1", ""); assert!(headers.is_ok()); let headers = headers.unwrap(); let user_agent = headers.get(USER_AGENT).unwrap().to_str().unwrap(); - assert_eq!(user_agent, "TestAgent/1.0"); + assert_eq!(user_agent, "GenevaUploader/0.1"); } #[test] diff --git a/opentelemetry-exporter-geneva/opentelemetry-exporter-geneva/examples/basic.rs b/opentelemetry-exporter-geneva/opentelemetry-exporter-geneva/examples/basic.rs index b0788dc62..d3e5f140b 100644 --- a/opentelemetry-exporter-geneva/opentelemetry-exporter-geneva/examples/basic.rs +++ b/opentelemetry-exporter-geneva/opentelemetry-exporter-geneva/examples/basic.rs @@ -63,7 +63,7 @@ async fn main() { role_name, role_instance, max_concurrent_uploads: None, // Use default - user_agent_suffix: Some("BasicExample".to_string()), + user_agent_suffix: Some("BasicExample"), }; let geneva_client = GenevaClient::new(config) From 7b2d972cda48816a19c55a0a08184ca08dbdb459 Mon Sep 17 00:00:00 2001 From: Lalit Kumar Bhasin Date: Fri, 1 Aug 2025 10:46:57 -0700 Subject: [PATCH 09/21] fix --- .../geneva-uploader/src/client.rs | 10 +++++----- .../geneva-uploader/src/config_service/client.rs | 10 ---------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs index d994a0690..9f4a400a3 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs @@ -23,16 +23,16 @@ pub struct GenevaClientConfig { pub role_instance: String, /// Maximum number of concurrent uploads. If None, defaults to number of CPU cores. pub max_concurrent_uploads: Option, - /// User agent for the application. Will be formatted as " (RustGenevaClient/0.1)". - /// If None, defaults to "RustGenevaClient/0.1". + /// User agent for the application. Will be formatted as " (GenevaUploader/0.1)". + /// If None, defaults to "GenevaUploader/0.1". /// /// The suffix must contain only ASCII printable characters, be non-empty (after trimming), /// and not exceed 200 characters in length. /// /// Examples: - /// - None: "RustGenevaClient/0.1" - /// - Some("MyApp/2.1.0"): "MyApp/2.1.0 (RustGenevaClient/0.1)" - /// - Some("ProductionService-1.0"): "ProductionService-1.0 (RustGenevaClient/0.1)" + /// - None: "GenevaUploader/0.1" + /// - Some("MyApp/2.1.0"): "MyApp/2.1.0 (GenevaUploader/0.1)" + /// - Some("ProductionService-1.0"): "ProductionService-1.0 (GenevaUploader/0.1)" pub user_agent_suffix: Option<&'static str>, // Add event name/version here if constant, or per-upload if you want them per call. } diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs index 27ef6b757..8f082b564 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs @@ -133,16 +133,6 @@ pub(crate) struct GenevaConfigClientConfig { pub(crate) region: String, pub(crate) config_major_version: u32, pub(crate) auth_method: AuthMethod, - /// User agent for the application. Will be formatted as " (GenevaUploader/0.1)". - /// If None, defaults to "GenevaUploader/0.1". - /// - /// The suffix must contain only ASCII printable characters, be non-empty (after trimming), - /// and not exceed 200 characters in length. - /// - /// Examples: - /// - None: "GenevaUploader/0.1" - /// - Some("MyApp/2.1.0"): "MyApp/2.1.0 (GenevaUploader/0.1)" - /// - Some("ProductionService-1.0"): "ProductionService-1.0 (GenevaUploader/0.1)" pub(crate) user_agent_suffix: Option<&'static str>, } From d912832863274cf17cd42333e6ddf592726b3616 Mon Sep 17 00:00:00 2001 From: Lalit Kumar Bhasin Date: Fri, 1 Aug 2025 11:24:36 -0700 Subject: [PATCH 10/21] fix --- .../geneva-uploader/src/client.rs | 8 +- .../src/config_service/client.rs | 154 ++---------------- .../src/ingestion_service/mod.rs | 2 +- .../geneva-uploader/src/lib.rs | 1 + .../examples/basic.rs | 2 +- stress/src/geneva_exporter.rs | 6 +- 6 files changed, 26 insertions(+), 147 deletions(-) diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs index 9f4a400a3..82966fb6b 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs @@ -23,17 +23,17 @@ pub struct GenevaClientConfig { pub role_instance: String, /// Maximum number of concurrent uploads. If None, defaults to number of CPU cores. pub max_concurrent_uploads: Option, - /// User agent for the application. Will be formatted as " (GenevaUploader/0.1)". + /// User agent prefix for the application. Will be formatted as " (GenevaUploader/0.1)". /// If None, defaults to "GenevaUploader/0.1". /// - /// The suffix must contain only ASCII printable characters, be non-empty (after trimming), + /// The prefix must contain only ASCII printable characters, be non-empty (after trimming), /// and not exceed 200 characters in length. /// /// Examples: /// - None: "GenevaUploader/0.1" /// - Some("MyApp/2.1.0"): "MyApp/2.1.0 (GenevaUploader/0.1)" /// - Some("ProductionService-1.0"): "ProductionService-1.0 (GenevaUploader/0.1)" - pub user_agent_suffix: Option<&'static str>, + pub user_agent_prefix: Option<&'static str>, // Add event name/version here if constant, or per-upload if you want them per call. } @@ -58,7 +58,7 @@ impl GenevaClient { region: cfg.region, config_major_version: cfg.config_major_version, auth_method: cfg.auth_method, - user_agent_suffix: cfg.user_agent_suffix, + user_agent_prefix: cfg.user_agent_prefix, }; let config_client = Arc::new( GenevaConfigClient::new(config_client_config) diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs index 8f082b564..32e9ca083 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs @@ -1,5 +1,6 @@ // Geneva Config Client with TLS (PKCS#12) and TODO: Managed Identity support +use crate::common::validate_user_agent_prefix; use base64::{engine::general_purpose, Engine as _}; use reqwest::{ header::{HeaderMap, HeaderValue, ACCEPT, USER_AGENT}, @@ -84,8 +85,8 @@ pub(crate) enum GenevaConfigClientError { SerdeJson(#[from] serde_json::Error), // Validation - #[error("Invalid user agent suffix: {0}")] - InvalidUserAgentSuffix(String), + #[error("Invalid user agent prefix: {0}")] + InvalidUserAgentPrefix(String), // Misc #[error("Moniker not found: {0}")] @@ -133,7 +134,7 @@ pub(crate) struct GenevaConfigClientConfig { pub(crate) region: String, pub(crate) config_major_version: u32, pub(crate) auth_method: AuthMethod, - pub(crate) user_agent_suffix: Option<&'static str>, + pub(crate) user_agent_prefix: Option<&'static str>, } #[allow(dead_code)] @@ -212,56 +213,6 @@ impl fmt::Debug for GenevaConfigClient { } } -/// Validates a user agent suffix for HTTP header compliance -/// -/// # Arguments -/// * `suffix` - The user agent suffix to validate -/// -/// # Returns -/// * `Ok(())` if valid -/// * `Err(GenevaConfigClientError::InvalidUserAgentSuffix)` if invalid -/// -/// # Validation Rules -/// - Must contain only ASCII printable characters (0x20-0x7E) -/// - Must not contain control characters (especially \r, \n, \0) -/// - Must not exceed 200 characters in length -/// - Must not be empty or only whitespace -fn validate_user_agent_suffix(suffix: &str) -> Result<()> { - if suffix.trim().is_empty() { - return Err(GenevaConfigClientError::InvalidUserAgentSuffix( - "User agent suffix cannot be empty or only whitespace".to_string(), - )); - } - - if suffix.len() > 200 { - return Err(GenevaConfigClientError::InvalidUserAgentSuffix(format!( - "User agent suffix too long: {len} characters (max 200)", - len = suffix.len() - ))); - } - - // Check for invalid characters - for (i, ch) in suffix.char_indices() { - match ch { - // Control characters that would break HTTP headers - '\r' | '\n' | '\0' => { - return Err(GenevaConfigClientError::InvalidUserAgentSuffix(format!( - "Invalid control character at position {i}: {ch:?}" - ))); - } - // Non-ASCII or non-printable characters - ch if !ch.is_ascii() || (ch as u8) < 0x20 || (ch as u8) > 0x7E => { - return Err(GenevaConfigClientError::InvalidUserAgentSuffix(format!( - "Invalid character at position {i}: {ch:?} (must be ASCII printable)" - ))); - } - _ => {} // Valid character - } - } - - Ok(()) -} - /// Client for interacting with the Geneva Configuration Service. /// /// This client handles authentication and communication with the Geneva Config @@ -280,9 +231,10 @@ impl GenevaConfigClient { /// * `GenevaConfigClientError::AuthMethodNotImplemented` - If the specified authentication method is not yet supported #[allow(dead_code)] pub(crate) fn new(config: GenevaConfigClientConfig) -> Result { - // Validate user_agent_suffix if provided - if let Some(suffix) = config.user_agent_suffix { - validate_user_agent_suffix(suffix)?; + // Validate user_agent_prefix if provided + if let Some(prefix) = config.user_agent_prefix { + validate_user_agent_prefix(prefix) + .map_err(|e| GenevaConfigClientError::InvalidUserAgentPrefix(e.to_string()))?; } let mut client_builder = Client::builder() @@ -321,9 +273,9 @@ impl GenevaConfigClient { let agent_identity = "GenevaUploader"; let agent_version = "0.1"; - let user_agent_suffix = config.user_agent_suffix.unwrap_or(""); + let user_agent_prefix = config.user_agent_prefix.unwrap_or(""); let static_headers = - Self::build_static_headers(agent_identity, agent_version, user_agent_suffix)?; + Self::build_static_headers(agent_identity, agent_version, user_agent_prefix)?; let identity = format!("Tenant=Default/Role=GcsClient/RoleInstance={agent_identity}"); @@ -367,18 +319,18 @@ impl GenevaConfigClient { fn build_static_headers( agent_identity: &str, agent_version: &str, - user_agent_suffix: &str, + user_agent_prefix: &str, ) -> Result { let mut headers = HeaderMap::new(); - let user_agent = if user_agent_suffix.is_empty() { + let user_agent = if user_agent_prefix.is_empty() { format!("{agent_identity}/{agent_version}") } else { - format!("{user_agent_suffix} ({agent_identity}/{agent_version})") + format!("{user_agent_prefix} ({agent_identity}/{agent_version})") }; // Safe header construction with proper error handling let header_value = HeaderValue::from_str(&user_agent).map_err(|e| { - GenevaConfigClientError::InvalidUserAgentSuffix(format!( + GenevaConfigClientError::InvalidUserAgentPrefix(format!( "Failed to create User-Agent header: {e}" )) })?; @@ -641,66 +593,6 @@ fn configure_tls_connector( mod tests { use super::*; - #[test] - fn test_validate_user_agent_suffix_valid() { - assert!(validate_user_agent_suffix("MyApp/1.0").is_ok()); - assert!(validate_user_agent_suffix("Production-Service-2.1.0").is_ok()); - assert!(validate_user_agent_suffix("TestApp_v3").is_ok()); - assert!(validate_user_agent_suffix("App-Name.1.2.3").is_ok()); - assert!(validate_user_agent_suffix("Simple123").is_ok()); - } - - #[test] - fn test_validate_user_agent_suffix_empty() { - assert!(validate_user_agent_suffix("").is_err()); - assert!(validate_user_agent_suffix(" ").is_err()); - assert!(validate_user_agent_suffix("\t\n").is_err()); - - if let Err(e) = validate_user_agent_suffix("") { - assert!(e.to_string().contains("cannot be empty")); - } - } - - #[test] - fn test_validate_user_agent_suffix_too_long() { - let long_suffix = "a".repeat(201); - let result = validate_user_agent_suffix(&long_suffix); - assert!(result.is_err()); - - if let Err(e) = result { - assert!(e.to_string().contains("too long")); - assert!(e.to_string().contains("201 characters")); - } - - // Test exactly at the limit should be OK - let max_length_suffix = "a".repeat(200); - assert!(validate_user_agent_suffix(&max_length_suffix).is_ok()); - } - - #[test] - fn test_validate_user_agent_suffix_invalid_chars() { - // Test control characters - assert!(validate_user_agent_suffix("App\nName").is_err()); - assert!(validate_user_agent_suffix("App\rName").is_err()); - assert!(validate_user_agent_suffix("App\0Name").is_err()); - assert!(validate_user_agent_suffix("App\tName").is_err()); - - // Test non-ASCII characters - assert!(validate_user_agent_suffix("App🚀Name").is_err()); - assert!(validate_user_agent_suffix("Appé").is_err()); - assert!(validate_user_agent_suffix("App中文").is_err()); - - // Test non-printable ASCII - assert!(validate_user_agent_suffix("App\x1FName").is_err()); // Unit separator - assert!(validate_user_agent_suffix("App\x7FName").is_err()); // DEL character - - // Verify error messages contain position information - if let Err(e) = validate_user_agent_suffix("App\nName") { - assert!(e.to_string().contains("position 3")); - assert!(e.to_string().contains("control character")); - } - } - #[test] fn test_build_static_headers_safe() { let headers = @@ -711,7 +603,7 @@ mod tests { let user_agent = headers.get(USER_AGENT).unwrap().to_str().unwrap(); assert_eq!(user_agent, "ValidApp/2.0 (GenevaUploader/0.1)"); - // Test empty suffix + // Test empty prefix let headers = GenevaConfigClient::build_static_headers("GenevaUploader", "0.1", ""); assert!(headers.is_ok()); @@ -724,25 +616,11 @@ mod tests { fn test_build_static_headers_invalid() { // This should not happen in practice due to validation, but test the safety mechanism let result = - GenevaConfigClient::build_static_headers("TestAgent", "1.0", "Invalid\nSuffix"); + GenevaConfigClient::build_static_headers("TestAgent", "1.0", "Invalid\nPrefix"); assert!(result.is_err()); if let Err(e) = result { assert!(e.to_string().contains("Failed to create User-Agent header")); } } - - #[test] - fn test_character_validation_edge_cases() { - // Test ASCII printable range boundaries - assert!(validate_user_agent_suffix(" ").is_err()); // Space only should be trimmed to empty - assert!(validate_user_agent_suffix("App Space").is_ok()); // Space in middle is OK - assert!(validate_user_agent_suffix("~").is_ok()); // Last printable ASCII (0x7E) - assert!(validate_user_agent_suffix("!").is_ok()); // First printable ASCII after space (0x21) - - // Test that spaces at the beginning and end are allowed (they're ASCII printable) - assert!(validate_user_agent_suffix(" ValidApp ").is_ok()); // Leading/trailing spaces are valid ASCII printable chars - // But strings that trim to empty should fail - assert!(validate_user_agent_suffix(" ").is_err()); // Only spaces should fail - } } diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/mod.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/mod.rs index 1d4949509..8eaa36e7a 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/mod.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/mod.rs @@ -64,7 +64,7 @@ mod tests { path: cert_path, password: cert_password, }, - user_agent_suffix: Some("TestUploader"), + user_agent_prefix: Some("TestUploader"), }; // Build client and uploader diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/lib.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/lib.rs index e322626cc..a6c51a21d 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/lib.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/lib.rs @@ -1,3 +1,4 @@ +mod common; mod config_service; mod ingestion_service; pub mod payload_encoder; diff --git a/opentelemetry-exporter-geneva/opentelemetry-exporter-geneva/examples/basic.rs b/opentelemetry-exporter-geneva/opentelemetry-exporter-geneva/examples/basic.rs index d3e5f140b..1ee170b67 100644 --- a/opentelemetry-exporter-geneva/opentelemetry-exporter-geneva/examples/basic.rs +++ b/opentelemetry-exporter-geneva/opentelemetry-exporter-geneva/examples/basic.rs @@ -63,7 +63,7 @@ async fn main() { role_name, role_instance, max_concurrent_uploads: None, // Use default - user_agent_suffix: Some("BasicExample"), + user_agent_prefix: Some("BasicExample"), }; let geneva_client = GenevaClient::new(config) diff --git a/stress/src/geneva_exporter.rs b/stress/src/geneva_exporter.rs index a03fc7f59..329a7b0a0 100644 --- a/stress/src/geneva_exporter.rs +++ b/stress/src/geneva_exporter.rs @@ -124,7 +124,7 @@ async fn init_client() -> Result<(GenevaClient, Option), Box Result<(GenevaClient, Option), Box Result<(GenevaClient, Option), Box Date: Fri, 1 Aug 2025 11:37:55 -0700 Subject: [PATCH 11/21] add missing common.rs --- .../geneva-uploader/src/common.rs | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 opentelemetry-exporter-geneva/geneva-uploader/src/common.rs diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/common.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/common.rs new file mode 100644 index 000000000..445c17617 --- /dev/null +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/common.rs @@ -0,0 +1,141 @@ +//! Common utilities and validation functions shared across the Geneva uploader crate. + +use thiserror::Error; + +/// Common validation errors +#[derive(Debug, Error)] +pub(crate) enum ValidationError { + #[error("Invalid user agent prefix: {0}")] + InvalidUserAgentPrefix(String), +} + +pub(crate) type Result = std::result::Result; + +/// Validates a user agent prefix for HTTP header compliance +/// +/// # Arguments +/// * `prefix` - The user agent prefix to validate +/// +/// # Returns +/// * `Ok(())` if valid +/// * `Err(ValidationError::InvalidUserAgentPrefix)` if invalid +/// +/// # Validation Rules +/// - Must contain only ASCII printable characters (0x20-0x7E) +/// - Must not contain control characters (especially \r, \n, \0) +/// - Must not exceed 200 characters in length +/// - Must not be empty or only whitespace +pub(crate) fn validate_user_agent_prefix(prefix: &str) -> Result<()> { + if prefix.trim().is_empty() { + return Err(ValidationError::InvalidUserAgentPrefix( + "User agent prefix cannot be empty or only whitespace".to_string(), + )); + } + + if prefix.len() > 200 { + return Err(ValidationError::InvalidUserAgentPrefix(format!( + "User agent prefix too long: {len} characters (max 200)", + len = prefix.len() + ))); + } + + // Check for invalid characters + for (i, ch) in prefix.char_indices() { + match ch { + // Control characters that would break HTTP headers + '\r' | '\n' | '\0' => { + return Err(ValidationError::InvalidUserAgentPrefix(format!( + "Invalid control character at position {i}: {ch:?}" + ))); + } + // Non-ASCII or non-printable characters + ch if !ch.is_ascii() || (ch as u8) < 0x20 || (ch as u8) > 0x7E => { + return Err(ValidationError::InvalidUserAgentPrefix(format!( + "Invalid character at position {i}: {ch:?} (must be ASCII printable)" + ))); + } + _ => {} // Valid character + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_validate_user_agent_prefix_valid() { + assert!(validate_user_agent_prefix("MyApp/1.0").is_ok()); + assert!(validate_user_agent_prefix("Production-Service-2.1.0").is_ok()); + assert!(validate_user_agent_prefix("TestApp_v3").is_ok()); + assert!(validate_user_agent_prefix("App-Name.1.2.3").is_ok()); + assert!(validate_user_agent_prefix("Simple123").is_ok()); + } + + #[test] + fn test_validate_user_agent_prefix_empty() { + assert!(validate_user_agent_prefix("").is_err()); + assert!(validate_user_agent_prefix(" ").is_err()); + assert!(validate_user_agent_prefix("\t\n").is_err()); + + if let Err(e) = validate_user_agent_prefix("") { + assert!(e.to_string().contains("cannot be empty")); + } + } + + #[test] + fn test_validate_user_agent_prefix_too_long() { + let long_prefix = "a".repeat(201); + let result = validate_user_agent_prefix(&long_prefix); + assert!(result.is_err()); + + if let Err(e) = result { + assert!(e.to_string().contains("too long")); + assert!(e.to_string().contains("201 characters")); + } + + // Test exactly at the limit should be OK + let max_length_prefix = "a".repeat(200); + assert!(validate_user_agent_prefix(&max_length_prefix).is_ok()); + } + + #[test] + fn test_validate_user_agent_prefix_invalid_chars() { + // Test control characters + assert!(validate_user_agent_prefix("App\nName").is_err()); + assert!(validate_user_agent_prefix("App\rName").is_err()); + assert!(validate_user_agent_prefix("App\0Name").is_err()); + assert!(validate_user_agent_prefix("App\tName").is_err()); + + // Test non-ASCII characters + assert!(validate_user_agent_prefix("App🚀Name").is_err()); + assert!(validate_user_agent_prefix("Appé").is_err()); + assert!(validate_user_agent_prefix("App中文").is_err()); + + // Test non-printable ASCII + assert!(validate_user_agent_prefix("AppName").is_err()); // Unit separator + assert!(validate_user_agent_prefix("AppName").is_err()); // DEL character + + // Verify error messages contain position information + if let Err(e) = validate_user_agent_prefix("App\nName") { + assert!(e.to_string().contains("position 3")); + assert!(e.to_string().contains("control character")); + } + } + + #[test] + fn test_character_validation_edge_cases() { + // Test ASCII printable range boundaries + assert!(validate_user_agent_prefix(" ").is_err()); // Space only should be trimmed to empty + assert!(validate_user_agent_prefix("App Space").is_ok()); // Space in middle is OK + assert!(validate_user_agent_prefix("~").is_ok()); // Last printable ASCII (0x7E) + assert!(validate_user_agent_prefix("!").is_ok()); // First printable ASCII after space (0x21) + + // Test that spaces at the beginning and end are allowed (they're ASCII printable) + assert!(validate_user_agent_prefix(" ValidApp ").is_ok()); // Leading/trailing spaces are valid ASCII printable chars + // But strings that trim to empty should fail + assert!(validate_user_agent_prefix(" ").is_err()); // Only spaces should fail + } +} From 5881c0680d7680b12396e0a73a74f6fc78cf072b Mon Sep 17 00:00:00 2001 From: Lalit Kumar Bhasin Date: Fri, 1 Aug 2025 13:30:53 -0700 Subject: [PATCH 12/21] add user-agent support in ingestion_service --- .../geneva-uploader/src/client.rs | 1 + .../geneva-uploader/src/common.rs | 73 ++++++++++++++++++- .../src/config_service/client.rs | 40 ++-------- .../geneva-uploader/src/config_service/mod.rs | 12 +-- .../src/ingestion_service/mod.rs | 1 + .../src/ingestion_service/uploader.rs | 23 ++++-- 6 files changed, 102 insertions(+), 48 deletions(-) diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs index 82966fb6b..63b6e7474 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs @@ -85,6 +85,7 @@ impl GenevaClient { source_identity, environment: cfg.environment, config_version: config_version.clone(), + user_agent_prefix: cfg.user_agent_prefix, }; let uploader = GenevaUploader::from_config_client(config_client, uploader_config) diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/common.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/common.rs index 445c17617..d32ef5539 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/common.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/common.rs @@ -1,5 +1,6 @@ //! Common utilities and validation functions shared across the Geneva uploader crate. +use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, USER_AGENT}; use thiserror::Error; /// Common validation errors @@ -61,6 +62,41 @@ pub(crate) fn validate_user_agent_prefix(prefix: &str) -> Result<()> { Ok(()) } +/// Builds static HTTP headers including User-Agent for Geneva clients +/// +/// # Arguments +/// * `agent_identity` - The identity of the agent (e.g., "GenevaUploader") +/// * `agent_version` - The version of the agent (e.g., "0.1") +/// * `user_agent_prefix` - Optional user agent prefix (can be empty string) +/// +/// # Returns +/// * `Result` - Headers with User-Agent and Accept headers set +/// +/// # User-Agent Format +/// - If prefix is empty: "{agent_identity}/{agent_version}" +/// - If prefix is provided: "{prefix} ({agent_identity}/{agent_version})" +pub(crate) fn build_static_headers( + agent_identity: &str, + agent_version: &str, + user_agent_prefix: &str, +) -> Result { + let mut headers = HeaderMap::new(); + let user_agent = if user_agent_prefix.is_empty() { + format!("{agent_identity}/{agent_version}") + } else { + format!("{user_agent_prefix} ({agent_identity}/{agent_version})") + }; + + // Safe header construction with proper error handling + let header_value = HeaderValue::from_str(&user_agent).map_err(|e| { + ValidationError::InvalidUserAgentPrefix(format!("Failed to create User-Agent header: {e}")) + })?; + + headers.insert(USER_AGENT, header_value); + headers.insert(ACCEPT, HeaderValue::from_static("application/json")); + Ok(headers) +} + #[cfg(test)] mod tests { use super::*; @@ -114,9 +150,11 @@ mod tests { assert!(validate_user_agent_prefix("Appé").is_err()); assert!(validate_user_agent_prefix("App中文").is_err()); - // Test non-printable ASCII - assert!(validate_user_agent_prefix("AppName").is_err()); // Unit separator - assert!(validate_user_agent_prefix("AppName").is_err()); // DEL character + // Test non-printable ASCII - construct strings with actual control characters + let unit_separator = format!("App{}Name", '\u{001F}'); + let del_char = format!("App{}Name", '\u{007F}'); + assert!(validate_user_agent_prefix(&unit_separator).is_err()); // Unit separator (0x1F) + assert!(validate_user_agent_prefix(&del_char).is_err()); // DEL character (0x7F) // Verify error messages contain position information if let Err(e) = validate_user_agent_prefix("App\nName") { @@ -138,4 +176,33 @@ mod tests { // But strings that trim to empty should fail assert!(validate_user_agent_prefix(" ").is_err()); // Only spaces should fail } + + #[test] + fn test_build_static_headers_safe() { + let headers = build_static_headers("GenevaUploader", "0.1", "ValidApp/2.0"); + assert!(headers.is_ok()); + + let headers = headers.unwrap(); + let user_agent = headers.get(USER_AGENT).unwrap().to_str().unwrap(); + assert_eq!(user_agent, "ValidApp/2.0 (GenevaUploader/0.1)"); + + // Test empty prefix + let headers = build_static_headers("GenevaUploader", "0.1", ""); + assert!(headers.is_ok()); + + let headers = headers.unwrap(); + let user_agent = headers.get(USER_AGENT).unwrap().to_str().unwrap(); + assert_eq!(user_agent, "GenevaUploader/0.1"); + } + + #[test] + fn test_build_static_headers_invalid() { + // This should not happen in practice due to validation, but test the safety mechanism + let result = build_static_headers("TestAgent", "1.0", "Invalid\nPrefix"); + assert!(result.is_err()); + + if let Err(e) = result { + assert!(e.to_string().contains("Failed to create User-Agent header")); + } + } } diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs index 32e9ca083..8529393cd 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs @@ -1,9 +1,9 @@ // Geneva Config Client with TLS (PKCS#12) and TODO: Managed Identity support -use crate::common::validate_user_agent_prefix; +use crate::common::{build_static_headers, validate_user_agent_prefix}; use base64::{engine::general_purpose, Engine as _}; use reqwest::{ - header::{HeaderMap, HeaderValue, ACCEPT, USER_AGENT}, + header::{HeaderMap, USER_AGENT}, Client, }; use serde::Deserialize; @@ -274,8 +274,8 @@ impl GenevaConfigClient { let agent_identity = "GenevaUploader"; let agent_version = "0.1"; let user_agent_prefix = config.user_agent_prefix.unwrap_or(""); - let static_headers = - Self::build_static_headers(agent_identity, agent_version, user_agent_prefix)?; + let static_headers = build_static_headers(agent_identity, agent_version, user_agent_prefix) + .map_err(|e| GenevaConfigClientError::InvalidUserAgentPrefix(e.to_string()))?; let identity = format!("Tenant=Default/Role=GcsClient/RoleInstance={agent_identity}"); @@ -316,30 +316,6 @@ impl GenevaConfigClient { .map(|dt| dt.with_timezone(&Utc)) } - fn build_static_headers( - agent_identity: &str, - agent_version: &str, - user_agent_prefix: &str, - ) -> Result { - let mut headers = HeaderMap::new(); - let user_agent = if user_agent_prefix.is_empty() { - format!("{agent_identity}/{agent_version}") - } else { - format!("{user_agent_prefix} ({agent_identity}/{agent_version})") - }; - - // Safe header construction with proper error handling - let header_value = HeaderValue::from_str(&user_agent).map_err(|e| { - GenevaConfigClientError::InvalidUserAgentPrefix(format!( - "Failed to create User-Agent header: {e}" - )) - })?; - - headers.insert(USER_AGENT, header_value); - headers.insert(ACCEPT, HeaderValue::from_static("application/json")); - Ok(headers) - } - /// Retrieves ingestion gateway information from the Geneva Config Service. /// /// # HTTP API Details @@ -595,8 +571,7 @@ mod tests { #[test] fn test_build_static_headers_safe() { - let headers = - GenevaConfigClient::build_static_headers("GenevaUploader", "0.1", "ValidApp/2.0"); + let headers = build_static_headers("GenevaUploader", "0.1", "ValidApp/2.0"); assert!(headers.is_ok()); let headers = headers.unwrap(); @@ -604,7 +579,7 @@ mod tests { assert_eq!(user_agent, "ValidApp/2.0 (GenevaUploader/0.1)"); // Test empty prefix - let headers = GenevaConfigClient::build_static_headers("GenevaUploader", "0.1", ""); + let headers = build_static_headers("GenevaUploader", "0.1", ""); assert!(headers.is_ok()); let headers = headers.unwrap(); @@ -615,8 +590,7 @@ mod tests { #[test] fn test_build_static_headers_invalid() { // This should not happen in practice due to validation, but test the safety mechanism - let result = - GenevaConfigClient::build_static_headers("TestAgent", "1.0", "Invalid\nPrefix"); + let result = build_static_headers("TestAgent", "1.0", "Invalid\nPrefix"); assert!(result.is_err()); if let Err(e) = result { diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/mod.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/mod.rs index 3c01545dd..e23f84a2c 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/mod.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/mod.rs @@ -21,7 +21,7 @@ mod tests { region: "region".to_string(), config_major_version: 1, auth_method: AuthMethod::ManagedIdentity, - user_agent_suffix: Some("TestApp/1.0"), + user_agent_prefix: Some("TestApp/1.0"), }; assert_eq!(config.environment, "env"); @@ -108,7 +108,7 @@ mod tests { path: PathBuf::from(temp_p12_file.path().to_string_lossy().to_string()), password, }, - user_agent_suffix: Some("MockClient/1.0"), + user_agent_prefix: Some("MockClient/1.0"), }; let client = GenevaConfigClient::new(config).unwrap(); @@ -154,7 +154,7 @@ mod tests { path: PathBuf::from(temp_p12_file.path().to_string_lossy().to_string()), password, }, - user_agent_suffix: Some("ErrorTestApp/1.0"), + user_agent_prefix: Some("ErrorTestApp/1.0"), }; let client = GenevaConfigClient::new(config).unwrap(); @@ -203,7 +203,7 @@ mod tests { path: PathBuf::from(temp_p12_file.path().to_string_lossy().to_string()), password, }, - user_agent_suffix: Some("MissingInfoTestApp/1.0"), + user_agent_prefix: Some("MissingInfoTestApp/1.0"), }; let client = GenevaConfigClient::new(config).unwrap(); @@ -235,7 +235,7 @@ mod tests { path: PathBuf::from("/nonexistent/path.p12".to_string()), password: "test".to_string(), }, - user_agent_suffix: Some("InvalidCertTestApp/1.0"), + user_agent_prefix: Some("InvalidCertTestApp/1.0"), }; let result = GenevaConfigClient::new(config); @@ -299,7 +299,7 @@ mod tests { path: PathBuf::from(cert_path), password: cert_password, }, - user_agent_suffix: Some("RealServerTestApp/1.0"), + user_agent_prefix: Some("RealServerTestApp/1.0"), }; println!("Connecting to real Geneva Config service..."); diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/mod.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/mod.rs index 8eaa36e7a..e3cbbe5d6 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/mod.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/mod.rs @@ -51,6 +51,7 @@ mod tests { source_identity, environment: environment.clone(), config_version, + user_agent_prefix: Some("TestUploader"), }; let config = GenevaConfigClientConfig { diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/uploader.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/uploader.rs index 5da88da59..af5f16174 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/uploader.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/uploader.rs @@ -1,3 +1,4 @@ +use crate::common::{build_static_headers, validate_user_agent_prefix}; use crate::config_service::client::{GenevaConfigClient, GenevaConfigClientError}; use crate::payload_encoder::central_blob::BatchMetadata; use reqwest::{header, Client}; @@ -27,6 +28,8 @@ pub(crate) enum GenevaUploaderError { #[allow(dead_code)] #[error("Internal error: {0}")] InternalError(String), + #[error("Invalid user agent prefix: {0}")] + InvalidUserAgentPrefix(String), } impl From for GenevaUploaderError { @@ -105,6 +108,7 @@ pub(crate) struct GenevaUploaderConfig { #[allow(dead_code)] pub environment: String, pub config_version: String, + pub user_agent_prefix: Option<&'static str>, } /// Client for uploading data to Geneva Ingestion Gateway (GIG) @@ -129,14 +133,21 @@ impl GenevaUploader { config_client: Arc, uploader_config: GenevaUploaderConfig, ) -> Result { - let mut headers = header::HeaderMap::new(); - headers.insert( - header::ACCEPT, - header::HeaderValue::from_static("application/json"), - ); + // Validate user_agent_prefix if provided + if let Some(prefix) = uploader_config.user_agent_prefix { + validate_user_agent_prefix(prefix) + .map_err(|e| GenevaUploaderError::InvalidUserAgentPrefix(e.to_string()))?; + } + + let agent_identity = "GenevaUploader"; + let agent_version = "0.1"; + let user_agent_prefix = uploader_config.user_agent_prefix.unwrap_or(""); + let static_headers = build_static_headers(agent_identity, agent_version, user_agent_prefix) + .map_err(|e| GenevaUploaderError::InvalidUserAgentPrefix(e.to_string()))?; + let http_client = Client::builder() .timeout(Duration::from_secs(30)) - .default_headers(headers) + .default_headers(static_headers) .build()?; Ok(Self { From f57c640a15b0f5d992d515d8eb922dc40aec275f Mon Sep 17 00:00:00 2001 From: Lalit Kumar Bhasin Date: Fri, 1 Aug 2025 14:01:08 -0700 Subject: [PATCH 13/21] fix warning --- .../geneva-uploader/src/config_service/client.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs index 8529393cd..285416a0a 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs @@ -3,7 +3,7 @@ use crate::common::{build_static_headers, validate_user_agent_prefix}; use base64::{engine::general_purpose, Engine as _}; use reqwest::{ - header::{HeaderMap, USER_AGENT}, + header::HeaderMap, Client, }; use serde::Deserialize; @@ -568,6 +568,7 @@ fn configure_tls_connector( #[cfg(test)] mod tests { use super::*; + use reqwest::header::USER_AGENT; #[test] fn test_build_static_headers_safe() { From 80e452877021a40a74eec5ad76df3bdc3ab019de Mon Sep 17 00:00:00 2001 From: Lalit Kumar Bhasin Date: Fri, 1 Aug 2025 14:12:28 -0700 Subject: [PATCH 14/21] fix --- .../geneva-uploader/src/client.rs | 118 ++++++++++++++++++ .../geneva-uploader/src/common.rs | 66 +--------- .../src/config_service/client.rs | 30 ++--- .../src/ingestion_service/uploader.rs | 10 +- 4 files changed, 138 insertions(+), 86 deletions(-) diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs index 63b6e7474..8f6a66a3e 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs @@ -1,11 +1,13 @@ //! High-level GenevaClient for user code. Wraps config_service and ingestion_service. +use crate::common::validate_user_agent_prefix; use crate::config_service::client::{AuthMethod, GenevaConfigClient, GenevaConfigClientConfig}; use crate::ingestion_service::uploader::{GenevaUploader, GenevaUploaderConfig}; use crate::payload_encoder::lz4_chunked_compression::lz4_chunked_compression; use crate::payload_encoder::otlp_encoder::OtlpEncoder; use futures::stream::{self, StreamExt}; use opentelemetry_proto::tonic::logs::v1::ResourceLogs; +use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT}; use std::sync::Arc; /// Configuration for GenevaClient (user-facing) @@ -37,6 +39,59 @@ pub struct GenevaClientConfig { // Add event name/version here if constant, or per-upload if you want them per call. } +/// Builds a standardized User-Agent header for Geneva services +/// +/// # Arguments +/// * `user_agent_prefix` - Optional user agent prefix from the client configuration +/// +/// # Returns +/// * `Result` - A properly formatted User-Agent header value +/// +/// # Format +/// - If prefix is None or empty: "GenevaUploader/0.1" +/// - If prefix is provided: "{prefix} (GenevaUploader/0.1)" +/// +/// # Example +/// ```ignore +/// let header = build_user_agent_header(Some("MyApp/2.1.0"))?; +/// // Results in: "MyApp/2.1.0 (GenevaUploader/0.1)" +/// ``` +pub fn build_user_agent_header(user_agent_prefix: Option<&str>) -> Result { + let prefix = user_agent_prefix.unwrap_or(""); + + // Validate the prefix if provided + if !prefix.is_empty() { + validate_user_agent_prefix(prefix) + .map_err(|e| format!("Invalid user agent prefix: {e}"))?; + } + + let user_agent = if prefix.is_empty() { + "GenevaUploader/0.1".to_string() + } else { + format!("{prefix} (GenevaUploader/0.1)") + }; + + HeaderValue::from_str(&user_agent) + .map_err(|e| format!("Failed to create User-Agent header: {e}")) +} + +/// Builds a complete set of HTTP headers for Geneva services +/// +/// # Arguments +/// * `user_agent_prefix` - Optional user agent prefix from the client configuration +/// +/// # Returns +/// * `Result` - HTTP headers including User-Agent and Accept +pub fn build_geneva_headers(user_agent_prefix: Option<&str>) -> Result { + let mut headers = HeaderMap::new(); + + let user_agent = build_user_agent_header(user_agent_prefix)?; + headers.insert(USER_AGENT, user_agent); + headers.insert("accept", HeaderValue::from_static("application/json")); + + Ok(headers) +} + /// Main user-facing client for Geneva ingestion. #[derive(Clone)] pub struct GenevaClient { @@ -145,3 +200,66 @@ impl GenevaClient { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use reqwest::header::USER_AGENT; + + #[test] + fn test_build_user_agent_header_without_prefix() { + let header = build_user_agent_header(None).unwrap(); + assert_eq!(header.to_str().unwrap(), "GenevaUploader/0.1"); + } + + #[test] + fn test_build_user_agent_header_with_empty_prefix() { + let header = build_user_agent_header(Some("")).unwrap(); + assert_eq!(header.to_str().unwrap(), "GenevaUploader/0.1"); + } + + #[test] + fn test_build_user_agent_header_with_valid_prefix() { + let header = build_user_agent_header(Some("MyApp/2.1.0")).unwrap(); + assert_eq!(header.to_str().unwrap(), "MyApp/2.1.0 (GenevaUploader/0.1)"); + } + + #[test] + fn test_build_user_agent_header_with_invalid_prefix() { + let result = build_user_agent_header(Some("Invalid\nPrefix")); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("Invalid user agent prefix")); + } + + #[test] + fn test_build_geneva_headers_complete() { + let headers = build_geneva_headers(Some("TestApp/1.0")).unwrap(); + + let user_agent = headers.get(USER_AGENT).unwrap(); + assert_eq!( + user_agent.to_str().unwrap(), + "TestApp/1.0 (GenevaUploader/0.1)" + ); + + let accept = headers.get("accept").unwrap(); + assert_eq!(accept.to_str().unwrap(), "application/json"); + } + + #[test] + fn test_build_geneva_headers_without_prefix() { + let headers = build_geneva_headers(None).unwrap(); + + let user_agent = headers.get(USER_AGENT).unwrap(); + assert_eq!(user_agent.to_str().unwrap(), "GenevaUploader/0.1"); + + let accept = headers.get("accept").unwrap(); + assert_eq!(accept.to_str().unwrap(), "application/json"); + } + + #[test] + fn test_build_geneva_headers_with_invalid_prefix() { + let result = build_geneva_headers(Some("Invalid\rPrefix")); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("Invalid user agent prefix")); + } +} diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/common.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/common.rs index d32ef5539..ce2db857b 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/common.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/common.rs @@ -1,6 +1,5 @@ //! Common utilities and validation functions shared across the Geneva uploader crate. -use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, USER_AGENT}; use thiserror::Error; /// Common validation errors @@ -62,40 +61,8 @@ pub(crate) fn validate_user_agent_prefix(prefix: &str) -> Result<()> { Ok(()) } -/// Builds static HTTP headers including User-Agent for Geneva clients -/// -/// # Arguments -/// * `agent_identity` - The identity of the agent (e.g., "GenevaUploader") -/// * `agent_version` - The version of the agent (e.g., "0.1") -/// * `user_agent_prefix` - Optional user agent prefix (can be empty string) -/// -/// # Returns -/// * `Result` - Headers with User-Agent and Accept headers set -/// -/// # User-Agent Format -/// - If prefix is empty: "{agent_identity}/{agent_version}" -/// - If prefix is provided: "{prefix} ({agent_identity}/{agent_version})" -pub(crate) fn build_static_headers( - agent_identity: &str, - agent_version: &str, - user_agent_prefix: &str, -) -> Result { - let mut headers = HeaderMap::new(); - let user_agent = if user_agent_prefix.is_empty() { - format!("{agent_identity}/{agent_version}") - } else { - format!("{user_agent_prefix} ({agent_identity}/{agent_version})") - }; - - // Safe header construction with proper error handling - let header_value = HeaderValue::from_str(&user_agent).map_err(|e| { - ValidationError::InvalidUserAgentPrefix(format!("Failed to create User-Agent header: {e}")) - })?; - - headers.insert(USER_AGENT, header_value); - headers.insert(ACCEPT, HeaderValue::from_static("application/json")); - Ok(headers) -} +// Note: build_static_headers has been moved to client.rs as build_geneva_headers +// for centralized user-agent building across all Geneva services #[cfg(test)] mod tests { @@ -177,32 +144,5 @@ mod tests { assert!(validate_user_agent_prefix(" ").is_err()); // Only spaces should fail } - #[test] - fn test_build_static_headers_safe() { - let headers = build_static_headers("GenevaUploader", "0.1", "ValidApp/2.0"); - assert!(headers.is_ok()); - - let headers = headers.unwrap(); - let user_agent = headers.get(USER_AGENT).unwrap().to_str().unwrap(); - assert_eq!(user_agent, "ValidApp/2.0 (GenevaUploader/0.1)"); - - // Test empty prefix - let headers = build_static_headers("GenevaUploader", "0.1", ""); - assert!(headers.is_ok()); - - let headers = headers.unwrap(); - let user_agent = headers.get(USER_AGENT).unwrap().to_str().unwrap(); - assert_eq!(user_agent, "GenevaUploader/0.1"); - } - - #[test] - fn test_build_static_headers_invalid() { - // This should not happen in practice due to validation, but test the safety mechanism - let result = build_static_headers("TestAgent", "1.0", "Invalid\nPrefix"); - assert!(result.is_err()); - - if let Err(e) = result { - assert!(e.to_string().contains("Failed to create User-Agent header")); - } - } + // Note: Header building tests have been moved to client.rs where the functionality now resides } diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs index 285416a0a..17b48bcc7 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs @@ -1,11 +1,9 @@ // Geneva Config Client with TLS (PKCS#12) and TODO: Managed Identity support -use crate::common::{build_static_headers, validate_user_agent_prefix}; +use crate::client::build_geneva_headers; +use crate::common::validate_user_agent_prefix; use base64::{engine::general_purpose, Engine as _}; -use reqwest::{ - header::HeaderMap, - Client, -}; +use reqwest::{header::HeaderMap, Client}; use serde::Deserialize; use std::time::Duration; use thiserror::Error; @@ -271,12 +269,10 @@ impl GenevaConfigClient { } } - let agent_identity = "GenevaUploader"; - let agent_version = "0.1"; - let user_agent_prefix = config.user_agent_prefix.unwrap_or(""); - let static_headers = build_static_headers(agent_identity, agent_version, user_agent_prefix) - .map_err(|e| GenevaConfigClientError::InvalidUserAgentPrefix(e.to_string()))?; + let static_headers = build_geneva_headers(config.user_agent_prefix) + .map_err(|e| GenevaConfigClientError::InvalidUserAgentPrefix(e))?; + let agent_identity = "GenevaUploader"; let identity = format!("Tenant=Default/Role=GcsClient/RoleInstance={agent_identity}"); let encoded_identity = general_purpose::STANDARD.encode(&identity); @@ -567,12 +563,12 @@ fn configure_tls_connector( #[cfg(test)] mod tests { - use super::*; + use crate::client::build_geneva_headers; use reqwest::header::USER_AGENT; #[test] - fn test_build_static_headers_safe() { - let headers = build_static_headers("GenevaUploader", "0.1", "ValidApp/2.0"); + fn test_build_geneva_headers_safe() { + let headers = build_geneva_headers(Some("ValidApp/2.0")); assert!(headers.is_ok()); let headers = headers.unwrap(); @@ -580,7 +576,7 @@ mod tests { assert_eq!(user_agent, "ValidApp/2.0 (GenevaUploader/0.1)"); // Test empty prefix - let headers = build_static_headers("GenevaUploader", "0.1", ""); + let headers = build_geneva_headers(None); assert!(headers.is_ok()); let headers = headers.unwrap(); @@ -589,13 +585,13 @@ mod tests { } #[test] - fn test_build_static_headers_invalid() { + fn test_build_geneva_headers_invalid() { // This should not happen in practice due to validation, but test the safety mechanism - let result = build_static_headers("TestAgent", "1.0", "Invalid\nPrefix"); + let result = build_geneva_headers(Some("Invalid\nPrefix")); assert!(result.is_err()); if let Err(e) = result { - assert!(e.to_string().contains("Failed to create User-Agent header")); + assert!(e.contains("Invalid user agent prefix")); } } } diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/uploader.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/uploader.rs index af5f16174..8d1d95e9d 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/uploader.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/uploader.rs @@ -1,4 +1,5 @@ -use crate::common::{build_static_headers, validate_user_agent_prefix}; +use crate::client::build_geneva_headers; +use crate::common::validate_user_agent_prefix; use crate::config_service::client::{GenevaConfigClient, GenevaConfigClientError}; use crate::payload_encoder::central_blob::BatchMetadata; use reqwest::{header, Client}; @@ -139,11 +140,8 @@ impl GenevaUploader { .map_err(|e| GenevaUploaderError::InvalidUserAgentPrefix(e.to_string()))?; } - let agent_identity = "GenevaUploader"; - let agent_version = "0.1"; - let user_agent_prefix = uploader_config.user_agent_prefix.unwrap_or(""); - let static_headers = build_static_headers(agent_identity, agent_version, user_agent_prefix) - .map_err(|e| GenevaUploaderError::InvalidUserAgentPrefix(e.to_string()))?; + let static_headers = build_geneva_headers(uploader_config.user_agent_prefix) + .map_err(|e| GenevaUploaderError::InvalidUserAgentPrefix(e))?; let http_client = Client::builder() .timeout(Duration::from_secs(30)) From d623496fa254430b5477ac78d70c925845d7b782 Mon Sep 17 00:00:00 2001 From: Lalit Kumar Bhasin Date: Fri, 1 Aug 2025 14:18:46 -0700 Subject: [PATCH 15/21] more rearrange --- .../geneva-uploader/src/client.rs | 118 ------------------ .../geneva-uploader/src/common.rs | 118 +++++++++++++++++- .../src/config_service/client.rs | 8 +- .../src/ingestion_service/uploader.rs | 4 +- 4 files changed, 121 insertions(+), 127 deletions(-) diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs index 8f6a66a3e..63b6e7474 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs @@ -1,13 +1,11 @@ //! High-level GenevaClient for user code. Wraps config_service and ingestion_service. -use crate::common::validate_user_agent_prefix; use crate::config_service::client::{AuthMethod, GenevaConfigClient, GenevaConfigClientConfig}; use crate::ingestion_service::uploader::{GenevaUploader, GenevaUploaderConfig}; use crate::payload_encoder::lz4_chunked_compression::lz4_chunked_compression; use crate::payload_encoder::otlp_encoder::OtlpEncoder; use futures::stream::{self, StreamExt}; use opentelemetry_proto::tonic::logs::v1::ResourceLogs; -use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT}; use std::sync::Arc; /// Configuration for GenevaClient (user-facing) @@ -39,59 +37,6 @@ pub struct GenevaClientConfig { // Add event name/version here if constant, or per-upload if you want them per call. } -/// Builds a standardized User-Agent header for Geneva services -/// -/// # Arguments -/// * `user_agent_prefix` - Optional user agent prefix from the client configuration -/// -/// # Returns -/// * `Result` - A properly formatted User-Agent header value -/// -/// # Format -/// - If prefix is None or empty: "GenevaUploader/0.1" -/// - If prefix is provided: "{prefix} (GenevaUploader/0.1)" -/// -/// # Example -/// ```ignore -/// let header = build_user_agent_header(Some("MyApp/2.1.0"))?; -/// // Results in: "MyApp/2.1.0 (GenevaUploader/0.1)" -/// ``` -pub fn build_user_agent_header(user_agent_prefix: Option<&str>) -> Result { - let prefix = user_agent_prefix.unwrap_or(""); - - // Validate the prefix if provided - if !prefix.is_empty() { - validate_user_agent_prefix(prefix) - .map_err(|e| format!("Invalid user agent prefix: {e}"))?; - } - - let user_agent = if prefix.is_empty() { - "GenevaUploader/0.1".to_string() - } else { - format!("{prefix} (GenevaUploader/0.1)") - }; - - HeaderValue::from_str(&user_agent) - .map_err(|e| format!("Failed to create User-Agent header: {e}")) -} - -/// Builds a complete set of HTTP headers for Geneva services -/// -/// # Arguments -/// * `user_agent_prefix` - Optional user agent prefix from the client configuration -/// -/// # Returns -/// * `Result` - HTTP headers including User-Agent and Accept -pub fn build_geneva_headers(user_agent_prefix: Option<&str>) -> Result { - let mut headers = HeaderMap::new(); - - let user_agent = build_user_agent_header(user_agent_prefix)?; - headers.insert(USER_AGENT, user_agent); - headers.insert("accept", HeaderValue::from_static("application/json")); - - Ok(headers) -} - /// Main user-facing client for Geneva ingestion. #[derive(Clone)] pub struct GenevaClient { @@ -200,66 +145,3 @@ impl GenevaClient { Ok(()) } } - -#[cfg(test)] -mod tests { - use super::*; - use reqwest::header::USER_AGENT; - - #[test] - fn test_build_user_agent_header_without_prefix() { - let header = build_user_agent_header(None).unwrap(); - assert_eq!(header.to_str().unwrap(), "GenevaUploader/0.1"); - } - - #[test] - fn test_build_user_agent_header_with_empty_prefix() { - let header = build_user_agent_header(Some("")).unwrap(); - assert_eq!(header.to_str().unwrap(), "GenevaUploader/0.1"); - } - - #[test] - fn test_build_user_agent_header_with_valid_prefix() { - let header = build_user_agent_header(Some("MyApp/2.1.0")).unwrap(); - assert_eq!(header.to_str().unwrap(), "MyApp/2.1.0 (GenevaUploader/0.1)"); - } - - #[test] - fn test_build_user_agent_header_with_invalid_prefix() { - let result = build_user_agent_header(Some("Invalid\nPrefix")); - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Invalid user agent prefix")); - } - - #[test] - fn test_build_geneva_headers_complete() { - let headers = build_geneva_headers(Some("TestApp/1.0")).unwrap(); - - let user_agent = headers.get(USER_AGENT).unwrap(); - assert_eq!( - user_agent.to_str().unwrap(), - "TestApp/1.0 (GenevaUploader/0.1)" - ); - - let accept = headers.get("accept").unwrap(); - assert_eq!(accept.to_str().unwrap(), "application/json"); - } - - #[test] - fn test_build_geneva_headers_without_prefix() { - let headers = build_geneva_headers(None).unwrap(); - - let user_agent = headers.get(USER_AGENT).unwrap(); - assert_eq!(user_agent.to_str().unwrap(), "GenevaUploader/0.1"); - - let accept = headers.get("accept").unwrap(); - assert_eq!(accept.to_str().unwrap(), "application/json"); - } - - #[test] - fn test_build_geneva_headers_with_invalid_prefix() { - let result = build_geneva_headers(Some("Invalid\rPrefix")); - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Invalid user agent prefix")); - } -} diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/common.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/common.rs index ce2db857b..b21b36424 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/common.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/common.rs @@ -1,5 +1,6 @@ //! Common utilities and validation functions shared across the Geneva uploader crate. +use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, USER_AGENT}; use thiserror::Error; /// Common validation errors @@ -61,8 +62,58 @@ pub(crate) fn validate_user_agent_prefix(prefix: &str) -> Result<()> { Ok(()) } -// Note: build_static_headers has been moved to client.rs as build_geneva_headers -// for centralized user-agent building across all Geneva services +/// Builds a standardized User-Agent header for Geneva services +/// +/// # Arguments +/// * `user_agent_prefix` - Optional user agent prefix from the client configuration +/// +/// # Returns +/// * `Result` - A properly formatted User-Agent header value +/// +/// # Format +/// - If prefix is None or empty: "GenevaUploader/0.1" +/// - If prefix is provided: "{prefix} (GenevaUploader/0.1)" +/// +/// # Example +/// ```ignore +/// let header = build_user_agent_header(Some("MyApp/2.1.0"))?; +/// // Results in: "MyApp/2.1.0 (GenevaUploader/0.1)" +/// ``` +pub(crate) fn build_user_agent_header(user_agent_prefix: Option<&str>) -> Result { + let prefix = user_agent_prefix.unwrap_or(""); + + // Validate the prefix if provided + if !prefix.is_empty() { + validate_user_agent_prefix(prefix)?; + } + + let user_agent = if prefix.is_empty() { + "GenevaUploader/0.1".to_string() + } else { + format!("{prefix} (GenevaUploader/0.1)") + }; + + HeaderValue::from_str(&user_agent).map_err(|e| { + ValidationError::InvalidUserAgentPrefix(format!("Failed to create User-Agent header: {e}")) + }) +} + +/// Builds a complete set of HTTP headers for Geneva services +/// +/// # Arguments +/// * `user_agent_prefix` - Optional user agent prefix from the client configuration +/// +/// # Returns +/// * `Result` - HTTP headers including User-Agent and Accept +pub(crate) fn build_geneva_headers(user_agent_prefix: Option<&str>) -> Result { + let mut headers = HeaderMap::new(); + + let user_agent = build_user_agent_header(user_agent_prefix)?; + headers.insert(USER_AGENT, user_agent); + headers.insert(ACCEPT, HeaderValue::from_static("application/json")); + + Ok(headers) +} #[cfg(test)] mod tests { @@ -144,5 +195,66 @@ mod tests { assert!(validate_user_agent_prefix(" ").is_err()); // Only spaces should fail } - // Note: Header building tests have been moved to client.rs where the functionality now resides + #[test] + fn test_build_user_agent_header_without_prefix() { + let header = build_user_agent_header(None).unwrap(); + assert_eq!(header.to_str().unwrap(), "GenevaUploader/0.1"); + } + + #[test] + fn test_build_user_agent_header_with_empty_prefix() { + let header = build_user_agent_header(Some("")).unwrap(); + assert_eq!(header.to_str().unwrap(), "GenevaUploader/0.1"); + } + + #[test] + fn test_build_user_agent_header_with_valid_prefix() { + let header = build_user_agent_header(Some("MyApp/2.1.0")).unwrap(); + assert_eq!(header.to_str().unwrap(), "MyApp/2.1.0 (GenevaUploader/0.1)"); + } + + #[test] + fn test_build_user_agent_header_with_invalid_prefix() { + let result = build_user_agent_header(Some("Invalid\nPrefix")); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("Invalid user agent prefix")); + } + + #[test] + fn test_build_geneva_headers_complete() { + let headers = build_geneva_headers(Some("TestApp/1.0")).unwrap(); + + let user_agent = headers.get(USER_AGENT).unwrap(); + assert_eq!( + user_agent.to_str().unwrap(), + "TestApp/1.0 (GenevaUploader/0.1)" + ); + + let accept = headers.get(ACCEPT).unwrap(); + assert_eq!(accept.to_str().unwrap(), "application/json"); + } + + #[test] + fn test_build_geneva_headers_without_prefix() { + let headers = build_geneva_headers(None).unwrap(); + + let user_agent = headers.get(USER_AGENT).unwrap(); + assert_eq!(user_agent.to_str().unwrap(), "GenevaUploader/0.1"); + + let accept = headers.get(ACCEPT).unwrap(); + assert_eq!(accept.to_str().unwrap(), "application/json"); + } + + #[test] + fn test_build_geneva_headers_with_invalid_prefix() { + let result = build_geneva_headers(Some("Invalid\rPrefix")); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("Invalid user agent prefix")); + } } diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs index 17b48bcc7..54b7309e6 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs @@ -1,6 +1,6 @@ // Geneva Config Client with TLS (PKCS#12) and TODO: Managed Identity support -use crate::client::build_geneva_headers; +use crate::common::build_geneva_headers; use crate::common::validate_user_agent_prefix; use base64::{engine::general_purpose, Engine as _}; use reqwest::{header::HeaderMap, Client}; @@ -270,7 +270,7 @@ impl GenevaConfigClient { } let static_headers = build_geneva_headers(config.user_agent_prefix) - .map_err(|e| GenevaConfigClientError::InvalidUserAgentPrefix(e))?; + .map_err(|e| GenevaConfigClientError::InvalidUserAgentPrefix(e.to_string()))?; let agent_identity = "GenevaUploader"; let identity = format!("Tenant=Default/Role=GcsClient/RoleInstance={agent_identity}"); @@ -563,7 +563,7 @@ fn configure_tls_connector( #[cfg(test)] mod tests { - use crate::client::build_geneva_headers; + use crate::common::build_geneva_headers; use reqwest::header::USER_AGENT; #[test] @@ -591,7 +591,7 @@ mod tests { assert!(result.is_err()); if let Err(e) = result { - assert!(e.contains("Invalid user agent prefix")); + assert!(e.to_string().contains("Invalid user agent prefix")); } } } diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/uploader.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/uploader.rs index 8d1d95e9d..f9ed741ab 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/uploader.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/uploader.rs @@ -1,4 +1,4 @@ -use crate::client::build_geneva_headers; +use crate::common::build_geneva_headers; use crate::common::validate_user_agent_prefix; use crate::config_service::client::{GenevaConfigClient, GenevaConfigClientError}; use crate::payload_encoder::central_blob::BatchMetadata; @@ -141,7 +141,7 @@ impl GenevaUploader { } let static_headers = build_geneva_headers(uploader_config.user_agent_prefix) - .map_err(|e| GenevaUploaderError::InvalidUserAgentPrefix(e))?; + .map_err(|e| GenevaUploaderError::InvalidUserAgentPrefix(e.to_string()))?; let http_client = Client::builder() .timeout(Duration::from_secs(30)) From 9cb1145686cfbb1da699eab302c5cb8d0e5f9229 Mon Sep 17 00:00:00 2001 From: Lalit Kumar Bhasin Date: Fri, 1 Aug 2025 14:23:41 -0700 Subject: [PATCH 16/21] fix --- .../src/config_service/client.rs | 35 +------------------ 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs index 54b7309e6..1002c86b7 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs @@ -561,37 +561,4 @@ fn configure_tls_connector( builder } -#[cfg(test)] -mod tests { - use crate::common::build_geneva_headers; - use reqwest::header::USER_AGENT; - - #[test] - fn test_build_geneva_headers_safe() { - let headers = build_geneva_headers(Some("ValidApp/2.0")); - assert!(headers.is_ok()); - - let headers = headers.unwrap(); - let user_agent = headers.get(USER_AGENT).unwrap().to_str().unwrap(); - assert_eq!(user_agent, "ValidApp/2.0 (GenevaUploader/0.1)"); - - // Test empty prefix - let headers = build_geneva_headers(None); - assert!(headers.is_ok()); - - let headers = headers.unwrap(); - let user_agent = headers.get(USER_AGENT).unwrap().to_str().unwrap(); - assert_eq!(user_agent, "GenevaUploader/0.1"); - } - - #[test] - fn test_build_geneva_headers_invalid() { - // This should not happen in practice due to validation, but test the safety mechanism - let result = build_geneva_headers(Some("Invalid\nPrefix")); - assert!(result.is_err()); - - if let Err(e) = result { - assert!(e.to_string().contains("Invalid user agent prefix")); - } - } -} +// Note: Tests for build_geneva_headers are in common.rs where the functionality is implemented From 3c02e0992e23418c388c8d11bf69c36fdbabb53c Mon Sep 17 00:00:00 2001 From: Lalit Kumar Bhasin Date: Fri, 1 Aug 2025 14:28:27 -0700 Subject: [PATCH 17/21] add todo --- .../geneva-uploader/src/common.rs | 31 +++++-------------- 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/common.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/common.rs index b21b36424..600ba6e97 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/common.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/common.rs @@ -62,23 +62,11 @@ pub(crate) fn validate_user_agent_prefix(prefix: &str) -> Result<()> { Ok(()) } -/// Builds a standardized User-Agent header for Geneva services -/// -/// # Arguments -/// * `user_agent_prefix` - Optional user agent prefix from the client configuration -/// -/// # Returns -/// * `Result` - A properly formatted User-Agent header value -/// -/// # Format -/// - If prefix is None or empty: "GenevaUploader/0.1" -/// - If prefix is provided: "{prefix} (GenevaUploader/0.1)" -/// -/// # Example -/// ```ignore -/// let header = build_user_agent_header(Some("MyApp/2.1.0"))?; -/// // Results in: "MyApp/2.1.0 (GenevaUploader/0.1)" -/// ``` +// Builds a standardized User-Agent header for Geneva services +// TODO: Update the user agent format based on whether custom config will come first or later +// Current format: +// - If prefix is None or empty: "GenevaUploader/0.1" +// - If prefix is provided: "{prefix} (GenevaUploader/0.1)" pub(crate) fn build_user_agent_header(user_agent_prefix: Option<&str>) -> Result { let prefix = user_agent_prefix.unwrap_or(""); @@ -98,13 +86,8 @@ pub(crate) fn build_user_agent_header(user_agent_prefix: Option<&str>) -> Result }) } -/// Builds a complete set of HTTP headers for Geneva services -/// -/// # Arguments -/// * `user_agent_prefix` - Optional user agent prefix from the client configuration -/// -/// # Returns -/// * `Result` - HTTP headers including User-Agent and Accept +// Builds a complete set of HTTP headers for Geneva services +// Returns HTTP headers including User-Agent and Accept pub(crate) fn build_geneva_headers(user_agent_prefix: Option<&str>) -> Result { let mut headers = HeaderMap::new(); From 683820c8b3c462323b538ae55b3b9e893b04ac8e Mon Sep 17 00:00:00 2001 From: Lalit Kumar Bhasin Date: Fri, 1 Aug 2025 14:31:02 -0700 Subject: [PATCH 18/21] fix --- .../geneva-uploader/src/common.rs | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/common.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/common.rs index 600ba6e97..0dbafba98 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/common.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/common.rs @@ -12,20 +12,12 @@ pub(crate) enum ValidationError { pub(crate) type Result = std::result::Result; -/// Validates a user agent prefix for HTTP header compliance -/// -/// # Arguments -/// * `prefix` - The user agent prefix to validate -/// -/// # Returns -/// * `Ok(())` if valid -/// * `Err(ValidationError::InvalidUserAgentPrefix)` if invalid -/// -/// # Validation Rules -/// - Must contain only ASCII printable characters (0x20-0x7E) -/// - Must not contain control characters (especially \r, \n, \0) -/// - Must not exceed 200 characters in length -/// - Must not be empty or only whitespace +// Validates a user agent prefix for HTTP header compliance +// Validation Rules: +// - Must contain only ASCII printable characters (0x20-0x7E) +// - Must not contain control characters (especially \r, \n, \0) +// - Must not exceed 200 characters in length +// - Must not be empty or only whitespace pub(crate) fn validate_user_agent_prefix(prefix: &str) -> Result<()> { if prefix.trim().is_empty() { return Err(ValidationError::InvalidUserAgentPrefix( From 1675d6cca7e937f49531ace9ad9072d74fce1750 Mon Sep 17 00:00:00 2001 From: Lalit Kumar Bhasin Date: Fri, 1 Aug 2025 14:46:10 -0700 Subject: [PATCH 19/21] reformat --- .../geneva-uploader/src/client.rs | 18 +++++++++++--- .../src/config_service/client.rs | 19 +++------------ .../geneva-uploader/src/config_service/mod.rs | 24 +++++++++++++++++++ .../src/ingestion_service/mod.rs | 8 ++++++- .../src/ingestion_service/uploader.rs | 18 +++----------- 5 files changed, 52 insertions(+), 35 deletions(-) diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs index 63b6e7474..95cdbdaf3 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs @@ -1,5 +1,6 @@ //! High-level GenevaClient for user code. Wraps config_service and ingestion_service. +use crate::common::{build_geneva_headers, validate_user_agent_prefix}; use crate::config_service::client::{AuthMethod, GenevaConfigClient, GenevaConfigClientConfig}; use crate::ingestion_service::uploader::{GenevaUploader, GenevaUploaderConfig}; use crate::payload_encoder::lz4_chunked_compression::lz4_chunked_compression; @@ -49,7 +50,17 @@ pub struct GenevaClient { impl GenevaClient { /// Construct a new client with minimal configuration. Fetches and caches ingestion info as needed. pub async fn new(cfg: GenevaClientConfig) -> Result { - // Build config client config + // Validate user agent prefix once and build headers once for both services + // This avoids duplicate validation and header building in config and ingestion services + if let Some(prefix) = cfg.user_agent_prefix { + validate_user_agent_prefix(prefix) + .map_err(|e| format!("Invalid user agent prefix: {e}"))?; + } + + let static_headers = build_geneva_headers(cfg.user_agent_prefix) + .map_err(|e| format!("Failed to build Geneva headers: {e}"))?; + + // Build config client config with pre-built headers let config_client_config = GenevaConfigClientConfig { endpoint: cfg.endpoint, environment: cfg.environment.clone(), @@ -59,6 +70,7 @@ impl GenevaClient { config_major_version: cfg.config_major_version, auth_method: cfg.auth_method, user_agent_prefix: cfg.user_agent_prefix, + static_headers: static_headers.clone(), }; let config_client = Arc::new( GenevaConfigClient::new(config_client_config) @@ -79,13 +91,13 @@ impl GenevaClient { cfg.namespace, config_version, cfg.tenant, cfg.role_name, cfg.role_instance, ); - // Uploader config + // Uploader config with pre-built headers let uploader_config = GenevaUploaderConfig { namespace: cfg.namespace.clone(), source_identity, environment: cfg.environment, config_version: config_version.clone(), - user_agent_prefix: cfg.user_agent_prefix, + static_headers: static_headers.clone(), }; let uploader = GenevaUploader::from_config_client(config_client, uploader_config) diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs index 1002c86b7..4de5ac01d 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs @@ -1,7 +1,5 @@ // Geneva Config Client with TLS (PKCS#12) and TODO: Managed Identity support -use crate::common::build_geneva_headers; -use crate::common::validate_user_agent_prefix; use base64::{engine::general_purpose, Engine as _}; use reqwest::{header::HeaderMap, Client}; use serde::Deserialize; @@ -82,10 +80,6 @@ pub(crate) enum GenevaConfigClientError { #[error("JSON error: {0}")] SerdeJson(#[from] serde_json::Error), - // Validation - #[error("Invalid user agent prefix: {0}")] - InvalidUserAgentPrefix(String), - // Misc #[error("Moniker not found: {0}")] MonikerNotFound(String), @@ -133,6 +127,7 @@ pub(crate) struct GenevaConfigClientConfig { pub(crate) config_major_version: u32, pub(crate) auth_method: AuthMethod, pub(crate) user_agent_prefix: Option<&'static str>, + pub(crate) static_headers: HeaderMap, } #[allow(dead_code)] @@ -229,12 +224,6 @@ impl GenevaConfigClient { /// * `GenevaConfigClientError::AuthMethodNotImplemented` - If the specified authentication method is not yet supported #[allow(dead_code)] pub(crate) fn new(config: GenevaConfigClientConfig) -> Result { - // Validate user_agent_prefix if provided - if let Some(prefix) = config.user_agent_prefix { - validate_user_agent_prefix(prefix) - .map_err(|e| GenevaConfigClientError::InvalidUserAgentPrefix(e.to_string()))?; - } - let mut client_builder = Client::builder() .http1_only() .timeout(Duration::from_secs(30)); //TODO - make this configurable @@ -269,9 +258,6 @@ impl GenevaConfigClient { } } - let static_headers = build_geneva_headers(config.user_agent_prefix) - .map_err(|e| GenevaConfigClientError::InvalidUserAgentPrefix(e.to_string()))?; - let agent_identity = "GenevaUploader"; let identity = format!("Tenant=Default/Role=GcsClient/RoleInstance={agent_identity}"); @@ -293,15 +279,16 @@ impl GenevaConfigClient { ).map_err(|e| GenevaConfigClientError::InternalError(format!("Failed to write URL: {e}")))?; let http_client = client_builder.build()?; + let static_headers = config.static_headers.clone(); Ok(Self { + static_headers, config, http_client, cached_data: RwLock::new(None), precomputed_url_prefix: pre_url, agent_identity: agent_identity.to_string(), // TODO make this configurable agent_version: "1.0".to_string(), // TODO make this configurable - static_headers, }) } diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/mod.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/mod.rs index e23f84a2c..a8a3d1b47 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/mod.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/mod.rs @@ -13,6 +13,9 @@ mod tests { #[test] fn test_config_fields() { + let static_headers = crate::common::build_geneva_headers(Some("TestApp/1.0")) + .expect("Failed to build Geneva headers"); + let config = GenevaConfigClientConfig { endpoint: "https://example.com".to_string(), environment: "env".to_string(), @@ -22,6 +25,7 @@ mod tests { config_major_version: 1, auth_method: AuthMethod::ManagedIdentity, user_agent_prefix: Some("TestApp/1.0"), + static_headers, }; assert_eq!(config.environment, "env"); @@ -97,6 +101,9 @@ mod tests { let (temp_p12_file, password) = generate_self_signed_p12(); + let static_headers = crate::common::build_geneva_headers(Some("MockClient/1.0")) + .expect("Failed to build Geneva headers"); + let config = GenevaConfigClientConfig { endpoint: mock_server.uri(), environment: "mockenv".into(), @@ -109,6 +116,7 @@ mod tests { password, }, user_agent_prefix: Some("MockClient/1.0"), + static_headers, }; let client = GenevaConfigClient::new(config).unwrap(); @@ -143,6 +151,9 @@ mod tests { let (temp_p12_file, password) = generate_self_signed_p12(); + let static_headers = crate::common::build_geneva_headers(Some("ErrorTestApp/1.0")) + .expect("Failed to build Geneva headers"); + let config = GenevaConfigClientConfig { endpoint: mock_server.uri(), environment: "mockenv".into(), @@ -155,6 +166,7 @@ mod tests { password, }, user_agent_prefix: Some("ErrorTestApp/1.0"), + static_headers, }; let client = GenevaConfigClient::new(config).unwrap(); @@ -192,6 +204,9 @@ mod tests { let (temp_p12_file, password) = generate_self_signed_p12(); + let static_headers = crate::common::build_geneva_headers(Some("MissingInfoTestApp/1.0")) + .expect("Failed to build Geneva headers"); + let config = GenevaConfigClientConfig { endpoint: mock_server.uri(), environment: "mockenv".into(), @@ -204,6 +219,7 @@ mod tests { password, }, user_agent_prefix: Some("MissingInfoTestApp/1.0"), + static_headers, }; let client = GenevaConfigClient::new(config).unwrap(); @@ -224,6 +240,9 @@ mod tests { #[cfg_attr(target_os = "macos", ignore)] // cert generated not compatible with macOS #[tokio::test] async fn test_invalid_certificate_path() { + let static_headers = crate::common::build_geneva_headers(Some("InvalidCertTestApp/1.0")) + .expect("Failed to build Geneva headers"); + let config = GenevaConfigClientConfig { endpoint: "https://example.com".to_string(), environment: "env".to_string(), @@ -236,6 +255,7 @@ mod tests { password: "test".to_string(), }, user_agent_prefix: Some("InvalidCertTestApp/1.0"), + static_headers, }; let result = GenevaConfigClient::new(config); @@ -288,6 +308,9 @@ mod tests { .parse::() // Convert string to u32 .expect("GENEVA_CONFIG_MAJOR_VERSION must be a valid unsigned integer"); + let static_headers = crate::common::build_geneva_headers(Some("RealServerTestApp/1.0")) + .expect("Failed to build Geneva headers"); + let config = GenevaConfigClientConfig { endpoint, environment, @@ -300,6 +323,7 @@ mod tests { password: cert_password, }, user_agent_prefix: Some("RealServerTestApp/1.0"), + static_headers, }; println!("Connecting to real Geneva Config service..."); diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/mod.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/mod.rs index e3cbbe5d6..16806447a 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/mod.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/mod.rs @@ -46,12 +46,17 @@ mod tests { // Define uploader config let config_version = format!("Ver{config_major_version}v0"); + + // Build the static headers once for both services + let static_headers = crate::common::build_geneva_headers(Some("TestUploader")) + .expect("Failed to build Geneva headers"); + let uploader_config = GenevaUploaderConfig { namespace: namespace.clone(), source_identity, environment: environment.clone(), config_version, - user_agent_prefix: Some("TestUploader"), + static_headers: static_headers.clone(), }; let config = GenevaConfigClientConfig { @@ -66,6 +71,7 @@ mod tests { password: cert_password, }, user_agent_prefix: Some("TestUploader"), + static_headers, }; // Build client and uploader diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/uploader.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/uploader.rs index f9ed741ab..639d6dcf9 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/uploader.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/uploader.rs @@ -1,7 +1,6 @@ -use crate::common::build_geneva_headers; -use crate::common::validate_user_agent_prefix; use crate::config_service::client::{GenevaConfigClient, GenevaConfigClientError}; use crate::payload_encoder::central_blob::BatchMetadata; +use reqwest::header::HeaderMap; use reqwest::{header, Client}; use serde::Deserialize; use serde_json::Value; @@ -29,8 +28,6 @@ pub(crate) enum GenevaUploaderError { #[allow(dead_code)] #[error("Internal error: {0}")] InternalError(String), - #[error("Invalid user agent prefix: {0}")] - InvalidUserAgentPrefix(String), } impl From for GenevaUploaderError { @@ -109,7 +106,7 @@ pub(crate) struct GenevaUploaderConfig { #[allow(dead_code)] pub environment: String, pub config_version: String, - pub user_agent_prefix: Option<&'static str>, + pub static_headers: HeaderMap, } /// Client for uploading data to Geneva Ingestion Gateway (GIG) @@ -134,18 +131,9 @@ impl GenevaUploader { config_client: Arc, uploader_config: GenevaUploaderConfig, ) -> Result { - // Validate user_agent_prefix if provided - if let Some(prefix) = uploader_config.user_agent_prefix { - validate_user_agent_prefix(prefix) - .map_err(|e| GenevaUploaderError::InvalidUserAgentPrefix(e.to_string()))?; - } - - let static_headers = build_geneva_headers(uploader_config.user_agent_prefix) - .map_err(|e| GenevaUploaderError::InvalidUserAgentPrefix(e.to_string()))?; - let http_client = Client::builder() .timeout(Duration::from_secs(30)) - .default_headers(static_headers) + .default_headers(uploader_config.static_headers.clone()) .build()?; Ok(Self { From c7121d1d184fde9cc07a9b049ce3980d9f9c01a6 Mon Sep 17 00:00:00 2001 From: Lalit Kumar Bhasin Date: Fri, 1 Aug 2025 14:49:04 -0700 Subject: [PATCH 20/21] build errors --- opentelemetry-exporter-geneva/geneva-uploader/src/client.rs | 1 - .../geneva-uploader/src/config_service/client.rs | 1 - .../geneva-uploader/src/config_service/mod.rs | 6 ------ .../geneva-uploader/src/ingestion_service/mod.rs | 1 - 4 files changed, 9 deletions(-) diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs index 95cdbdaf3..0118debbb 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs @@ -69,7 +69,6 @@ impl GenevaClient { region: cfg.region, config_major_version: cfg.config_major_version, auth_method: cfg.auth_method, - user_agent_prefix: cfg.user_agent_prefix, static_headers: static_headers.clone(), }; let config_client = Arc::new( diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs index 4de5ac01d..8f64b8e95 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs @@ -126,7 +126,6 @@ pub(crate) struct GenevaConfigClientConfig { pub(crate) region: String, pub(crate) config_major_version: u32, pub(crate) auth_method: AuthMethod, - pub(crate) user_agent_prefix: Option<&'static str>, pub(crate) static_headers: HeaderMap, } diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/mod.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/mod.rs index a8a3d1b47..d70718d46 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/mod.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/mod.rs @@ -24,7 +24,6 @@ mod tests { region: "region".to_string(), config_major_version: 1, auth_method: AuthMethod::ManagedIdentity, - user_agent_prefix: Some("TestApp/1.0"), static_headers, }; @@ -115,7 +114,6 @@ mod tests { path: PathBuf::from(temp_p12_file.path().to_string_lossy().to_string()), password, }, - user_agent_prefix: Some("MockClient/1.0"), static_headers, }; @@ -165,7 +163,6 @@ mod tests { path: PathBuf::from(temp_p12_file.path().to_string_lossy().to_string()), password, }, - user_agent_prefix: Some("ErrorTestApp/1.0"), static_headers, }; @@ -218,7 +215,6 @@ mod tests { path: PathBuf::from(temp_p12_file.path().to_string_lossy().to_string()), password, }, - user_agent_prefix: Some("MissingInfoTestApp/1.0"), static_headers, }; @@ -254,7 +250,6 @@ mod tests { path: PathBuf::from("/nonexistent/path.p12".to_string()), password: "test".to_string(), }, - user_agent_prefix: Some("InvalidCertTestApp/1.0"), static_headers, }; @@ -322,7 +317,6 @@ mod tests { path: PathBuf::from(cert_path), password: cert_password, }, - user_agent_prefix: Some("RealServerTestApp/1.0"), static_headers, }; diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/mod.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/mod.rs index 16806447a..ea0e08020 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/mod.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/ingestion_service/mod.rs @@ -70,7 +70,6 @@ mod tests { path: cert_path, password: cert_password, }, - user_agent_prefix: Some("TestUploader"), static_headers, }; From 2ec20c4f640f716e9e8afc87b8897c3f60a4e98c Mon Sep 17 00:00:00 2001 From: Lalit Kumar Bhasin Date: Fri, 1 Aug 2025 14:50:29 -0700 Subject: [PATCH 21/21] fix --- .../geneva-uploader/src/config_service/client.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs index 8f64b8e95..5b897dbc3 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs @@ -546,5 +546,3 @@ fn configure_tls_connector( .max_protocol_version(Some(Protocol::Tlsv12)); builder } - -// Note: Tests for build_geneva_headers are in common.rs where the functionality is implemented