-
Notifications
You must be signed in to change notification settings - Fork 69
feat: [Geneva Exporter] Add configurable User-Agent Header #400
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
lalitb
wants to merge
34
commits into
open-telemetry:main
Choose a base branch
from
lalitb:custom-user-agent
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 32 commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
d0792f3
fix
lalitb 880d6e5
fix
lalitb 0a60b82
fix
lalitb 78be687
more changes
lalitb a0797f7
Merge branch 'main' into custom-user-agent
lalitb 99f10d7
fix
lalitb 2806cf3
Merge branch 'custom-user-agent' of github.com:lalitb/opentelemetry-r…
lalitb 606caf6
Merge branch 'main' into custom-user-agent
lalitb 76da83c
fix
lalitb a381d16
Merge branch 'custom-user-agent' of github.com:lalitb/opentelemetry-r…
lalitb 50d94cd
fix
lalitb 7ecda1b
fix
lalitb 7b2d972
fix
lalitb d912832
fix
lalitb 504845e
add missing common.rs
lalitb 5881c06
add user-agent support in ingestion_service
lalitb f57c640
fix warning
lalitb 80e4528
fix
lalitb d623496
more rearrange
lalitb 9cb1145
fix
lalitb 3c02e09
add todo
lalitb 683820c
fix
lalitb 1675d6c
reformat
lalitb c7121d1
build errors
lalitb 2ec20c4
fix
lalitb 6ab85ab
Merge branch 'main' into custom-user-agent
lalitb 6fb82cb
Merge branch 'main' into custom-user-agent
lalitb 643e5ab
Merge branch 'main' into custom-user-agent
lalitb e2d84be
remove non-ascii validation
lalitb f5d840d
fix nit
lalitb ca74e81
nit doc
lalitb a2d68e4
more build fix
lalitb 143548a
doc fix
lalitb aa6fb5e
fix
lalitb File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,6 @@ | ||||||||||||||||||||||||||||||||||||||||||
| //! High-level GenevaClient for user code. Wraps config_service and ingestion_service. | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| use crate::common::build_geneva_headers; | ||||||||||||||||||||||||||||||||||||||||||
| use crate::config_service::client::{AuthMethod, GenevaConfigClient, GenevaConfigClientConfig}; | ||||||||||||||||||||||||||||||||||||||||||
| // ManagedIdentitySelector removed; no re-export needed. | ||||||||||||||||||||||||||||||||||||||||||
| use crate::ingestion_service::uploader::{GenevaUploader, GenevaUploaderConfig}; | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -31,6 +32,17 @@ pub struct GenevaClientConfig { | |||||||||||||||||||||||||||||||||||||||||
| pub tenant: String, | ||||||||||||||||||||||||||||||||||||||||||
| pub role_name: String, | ||||||||||||||||||||||||||||||||||||||||||
| pub role_instance: String, | ||||||||||||||||||||||||||||||||||||||||||
| /// User agent prefix for the application. Will be formatted as "<prefix> (GenevaUploader/0.1)". | ||||||||||||||||||||||||||||||||||||||||||
| /// If None, defaults to "GenevaUploader/0.1". | ||||||||||||||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||||||||||||||
| /// 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)" | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
| /// User agent prefix for the application. Will be formatted as "<prefix> (GenevaUploader/0.1)". | |
| /// If None, defaults to "GenevaUploader/0.1". | |
| /// | |
| /// 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)" | |
| /// User agent prefix for the application. Will be formatted as "<prefix> (RustGenevaClient/0.1)". | |
| /// If None, defaults to "RustGenevaClient/0.1". | |
| /// | |
| /// The prefix 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)" |
181 changes: 181 additions & 0 deletions
181
opentelemetry-exporter-geneva/geneva-uploader/src/common.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| //! 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 | ||
| #[derive(Debug, Error)] | ||
| pub(crate) enum ValidationError { | ||
| #[error("Invalid user agent: {0}")] | ||
| InvalidUserAgent(String), | ||
| } | ||
|
|
||
| pub(crate) type Result<T> = std::result::Result<T, ValidationError>; | ||
|
|
||
| // Builds a standardized User-Agent header for Geneva services | ||
| // Format: | ||
| // - If prefix is None or empty: "RustGenevaClient/0.1" | ||
| // - If prefix is provided: "{prefix} (RustGenevaClient/0.1)" | ||
| // | ||
| // Validation: | ||
| // - HeaderValue::from_str() automatically rejects control characters (\r, \n, \0) | ||
| // - We additionally verify the header can be represented as ASCII via to_str() | ||
| pub(crate) fn build_user_agent_header(user_agent_prefix: Option<&str>) -> Result<HeaderValue> { | ||
| let prefix = user_agent_prefix.unwrap_or(""); | ||
|
|
||
| // Basic validation - length and non-empty checks | ||
| if !prefix.is_empty() { | ||
| if prefix.trim().is_empty() { | ||
| return Err(ValidationError::InvalidUserAgent( | ||
| "User agent prefix cannot be only whitespace".to_string(), | ||
| )); | ||
| } | ||
| if prefix.len() > 200 { | ||
| return Err(ValidationError::InvalidUserAgent(format!( | ||
| "User agent prefix too long: {} characters (max 200)", | ||
| prefix.len() | ||
| ))); | ||
| } | ||
| } | ||
|
|
||
| // Optimize for the no-prefix case - avoid allocation | ||
| let header_value = if prefix.is_empty() { | ||
| HeaderValue::from_static("RustGenevaClient/0.1") | ||
| } else { | ||
| let user_agent = format!("{prefix} (RustGenevaClient/0.1)"); | ||
| let header_value = HeaderValue::from_str(&user_agent).map_err(|e| { | ||
| ValidationError::InvalidUserAgent(format!("Invalid User-Agent header: {e}")) | ||
| })?; | ||
|
|
||
| // Verify the header can be represented as valid ASCII string | ||
| // This rejects non-ASCII characters like emojis, Chinese chars, etc. | ||
| header_value.to_str().map_err(|_| { | ||
| ValidationError::InvalidUserAgent( | ||
| "User-Agent contains non-ASCII characters".to_string(), | ||
| ) | ||
| })?; | ||
|
|
||
| header_value | ||
| }; | ||
|
|
||
| Ok(header_value) | ||
| } | ||
|
|
||
| // 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<HeaderMap> { | ||
| 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 { | ||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn test_build_user_agent_header_without_prefix() { | ||
| let header = build_user_agent_header(None).unwrap(); | ||
| assert_eq!(header.to_str().unwrap(), "RustGenevaClient/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(), "RustGenevaClient/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 (RustGenevaClient/0.1)" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_build_user_agent_header_with_invalid_control_chars() { | ||
| // Control characters are automatically rejected by HeaderValue::from_str() | ||
| assert!(build_user_agent_header(Some("Invalid\nPrefix")).is_err()); | ||
| assert!(build_user_agent_header(Some("App\rName")).is_err()); | ||
| assert!(build_user_agent_header(Some("App\0Name")).is_err()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_build_user_agent_header_with_non_ascii() { | ||
| // Non-ASCII characters should be rejected because we validate with to_str() | ||
| assert!(build_user_agent_header(Some("App€Name")).is_err()); | ||
| assert!(build_user_agent_header(Some("App🌏Name")).is_err()); | ||
| assert!(build_user_agent_header(Some("App🚀Name")).is_err()); | ||
| assert!(build_user_agent_header(Some("App中文Name")).is_err()); | ||
|
|
||
| // Verify error message mentions non-ASCII | ||
| let result = build_user_agent_header(Some("App中文")); | ||
| assert!(result.is_err()); | ||
| assert!(result.unwrap_err().to_string().contains("non-ASCII")); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_build_user_agent_header_length_validation() { | ||
| // Test too long prefix | ||
| let long_prefix = "a".repeat(201); | ||
| let result = build_user_agent_header(Some(&long_prefix)); | ||
| assert!(result.is_err()); | ||
| assert!(result.unwrap_err().to_string().contains("too long")); | ||
|
|
||
| // Test exactly at the limit | ||
| let max_prefix = "a".repeat(200); | ||
| assert!(build_user_agent_header(Some(&max_prefix)).is_ok()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_build_user_agent_header_whitespace_validation() { | ||
| // Only whitespace should fail | ||
| assert!(build_user_agent_header(Some(" ")).is_err()); | ||
| assert!(build_user_agent_header(Some("\t")).is_err()); | ||
|
|
||
| // Whitespace within valid text is OK | ||
| assert!(build_user_agent_header(Some("My App")).is_ok()); | ||
| assert!(build_user_agent_header(Some(" MyApp ")).is_ok()); | ||
| } | ||
|
|
||
| #[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 (RustGenevaClient/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(), "RustGenevaClient/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")); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation states the format will use 'GenevaUploader/0.1' but the actual implementation in
build_user_agent_headeruses 'RustGenevaClient/0.1'. This inconsistency between documented and actual behavior should be corrected to match the implementation.