1use crate::config_service::client::{AuthMethod, GenevaConfigClient, GenevaConfigClientConfig};
4use crate::ingestion_service::uploader::{
6 GenevaUploader, GenevaUploaderConfig, GenevaUploaderError,
7};
8use crate::payload_encoder::otlp_encoder::OtlpEncoder;
9use crate::payload_encoder::otlp_encoder::{lookup_obo_config, MetadataFields};
10pub use crate::payload_encoder::otlp_encoder::{OboEventConfig, OboEventMap};
11#[cfg(test)]
12use crate::payload_encoder::routing::{
13 resolve_span_event_name, span_routing_value_from_attributes, span_routing_value_from_resource,
14 span_routing_value_from_scope, SCOPE_NAME_ROUTING_KEY, SCOPE_VERSION_ROUTING_KEY,
15};
16use crate::payload_encoder::routing::{
17 resolve_span_event_name_in_scope, resolve_span_scope_routing,
18};
19pub use crate::payload_encoder::routing::{
20 LogsEventNameMapping, LogsEventNameRoutingKey, SpanEventNameMapping, SpanEventNameRoutingKey,
21};
22#[cfg(test)]
23use opentelemetry_proto::tonic::common::v1::{InstrumentationScope, KeyValue};
24#[cfg(test)]
25use opentelemetry_proto::tonic::resource::v1::Resource;
26use opentelemetry_proto::tonic::trace::v1::{ResourceSpans, Span};
27use otap_df_pdata_views::views::logs::LogsDataView;
28use secrecy::{ExposeSecret, SecretString};
29use std::collections::HashMap;
30use std::fmt;
31use std::future::Future;
32use std::pin::Pin;
33use std::sync::Arc;
34use std::time::Duration;
35use tracing::{debug, info};
36
37#[derive(Debug, Clone, PartialEq)]
40pub struct EncodedBatch {
41 pub event_name: String,
42 pub account_group: String,
43 pub(crate) data: Vec<u8>,
44 pub(crate) metadata: crate::payload_encoder::central_blob::BatchMetadata,
45 pub row_count: usize,
46}
47
48impl EncodedBatch {
49 #[must_use]
51 pub fn compressed_size(&self) -> usize {
52 self.data.len()
53 }
54}
55
56#[derive(Clone, Debug)]
58pub struct GenevaClientConfig {
59 pub endpoint: String,
60 pub environment: String,
61 pub account: String,
62 pub namespace: String,
63 pub account_routing: AccountRouting,
64 pub region: String,
65 pub config_major_version: u32,
66 pub auth_method: AuthMethod,
67 pub tenant: String,
68 pub role_name: String,
69 pub role_instance: String,
70 pub msi_resource: Option<String>, pub logs: Option<LogsConfig>,
72 pub spans: Option<TracesConfig>,
73 pub obo_event_map: Option<OboEventMap>, }
75
76#[derive(Clone, Debug)]
84pub struct AccountRouting {
85 default_group: String,
86 groups_by_event: HashMap<String, String>,
87}
88
89impl AccountRouting {
90 pub fn new(default_group: impl Into<String>) -> Self {
91 Self {
92 default_group: default_group.into(),
93 groups_by_event: HashMap::new(),
94 }
95 }
96
97 pub fn with_event_group(
98 mut self,
99 event_name: impl Into<String>,
100 account_group: impl Into<String>,
101 ) -> Self {
102 self.groups_by_event
103 .insert(event_name.into(), account_group.into());
104 self
105 }
106
107 pub fn with_event_groups(mut self, map: HashMap<String, String>) -> Self {
108 self.groups_by_event.extend(map);
109 self
110 }
111
112 fn validate(&self) -> Result<(), String> {
113 if self.default_group.trim().is_empty() {
114 return Err("account_routing.default_group must not be empty".to_string());
115 }
116 for (event, group) in &self.groups_by_event {
117 if event.trim().is_empty() || group.trim().is_empty() {
118 return Err("account_routing event and group names must not be empty".to_string());
119 }
120 }
121 Ok(())
122 }
123
124 fn group_for_event(&self, event_name: &str) -> &str {
125 self.groups_by_event
126 .get(event_name)
127 .map_or(self.default_group.as_str(), String::as_str)
128 }
129}
130
131#[derive(Clone, Debug)]
132pub struct LogsConfig {
133 pub default_event_name: Option<String>,
134 pub event_name_mapping: Option<LogsEventNameMapping>,
135}
136
137#[derive(Clone, Debug)]
138pub struct TracesConfig {
139 pub default_event_name: Option<String>,
140 pub event_name_mapping: Option<SpanEventNameMapping>,
141}
142
143pub trait AgentFedCredentialSource: Send + Sync + std::fmt::Debug {
148 fn current(&self) -> AgentFedCredentialFuture<'_>;
157}
158
159pub type AgentFedCredentialFuture<'a> =
161 Pin<Box<dyn Future<Output = Option<AgentFedCredential>> + Send + 'a>>;
162
163#[derive(Clone)]
165pub struct AgentFedCredential {
166 token: SecretString,
168 pub endpoint: String,
170 pub primary_monikers: HashMap<String, String>,
172}
173
174impl AgentFedCredential {
175 #[must_use]
177 pub fn new(
178 token: impl Into<SecretString>,
179 endpoint: impl Into<String>,
180 primary_monikers: HashMap<String, String>,
181 ) -> Self {
182 Self {
183 token: token.into(),
184 endpoint: endpoint.into(),
185 primary_monikers,
186 }
187 }
188
189 #[must_use]
191 pub fn expose_token(&self) -> &str {
192 self.token.expose_secret()
193 }
194
195 pub(crate) fn into_parts(self) -> (SecretString, String, HashMap<String, String>) {
196 (self.token, self.endpoint, self.primary_monikers)
197 }
198}
199
200impl std::fmt::Debug for AgentFedCredential {
201 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202 f.debug_struct("AgentFedCredential")
204 .field("token", &"<redacted>")
205 .field("endpoint", &self.endpoint)
206 .field("account_groups", &self.primary_monikers.keys())
207 .finish()
208 }
209}
210
211#[derive(Debug)]
223pub enum UploadError {
224 HttpStatus {
226 status: u16,
227 retry_after: Option<Duration>,
228 message: String,
229 },
230 Transport(String),
232 AccountGroupNotResolved {
236 requested: String,
237 available: Vec<String>,
238 },
239 Other(String),
241}
242
243impl fmt::Display for UploadError {
244 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245 match self {
246 Self::HttpStatus {
247 status, message, ..
248 } => {
249 write!(f, "upload failed with status {status}: {message}")
250 }
251 Self::Transport(msg) => write!(f, "transport error: {msg}"),
252 Self::AccountGroupNotResolved {
253 requested,
254 available,
255 } => write!(
256 f,
257 "account group '{requested}' was not resolved; available groups: {available:?}"
258 ),
259 Self::Other(msg) => write!(f, "{msg}"),
260 }
261 }
262}
263
264impl std::error::Error for UploadError {}
265
266#[derive(Clone)]
268pub struct GenevaClient {
269 uploader: Arc<GenevaUploader>,
270 encoder: OtlpEncoder,
271 metadata_fields: MetadataFields,
272 log_table_name: Arc<str>,
273 log_event_name_mapping: Option<LogsEventNameMapping>,
274 span_table_name: Arc<str>,
275 span_event_name_mapping: Option<SpanEventNameMapping>,
276 obo_event_map: Option<OboEventMap>,
277 account_routing: AccountRouting,
278}
279
280struct ClientParts {
283 log_table_name: Arc<str>,
284 log_event_name_mapping: Option<LogsEventNameMapping>,
285 span_table_name: Arc<str>,
286 span_event_name_mapping: Option<SpanEventNameMapping>,
287 metadata_fields: MetadataFields,
288 obo_event_map: Option<OboEventMap>,
289 account_routing: AccountRouting,
290}
291
292impl GenevaClient {
293 fn validate_event_name_mappings(cfg: &GenevaClientConfig) -> Result<(), String> {
295 cfg.account_routing.validate()?;
296 if let Some(mapping) = cfg
297 .logs
298 .as_ref()
299 .and_then(|logs| logs.event_name_mapping.as_ref())
300 {
301 mapping.validate()?;
302 }
303 if let Some(mapping) = cfg
304 .spans
305 .as_ref()
306 .and_then(|spans| spans.event_name_mapping.as_ref())
307 {
308 mapping.validate()?;
309 }
310 Ok(())
311 }
312
313 fn derive_parts(cfg: GenevaClientConfig) -> (GenevaUploaderConfig, ClientParts) {
316 let GenevaClientConfig {
317 environment,
318 namespace,
319 config_major_version,
320 tenant,
321 role_name,
322 role_instance,
323 logs,
324 spans,
325 obo_event_map,
326 account_routing,
327 ..
328 } = cfg;
329
330 let spans_present = spans.is_some();
331 let logs_present = logs.is_some();
332 let (log_default_event_name, log_event_name_mapping) = match logs {
333 Some(logs) => (logs.default_event_name, logs.event_name_mapping),
334 None => (None, None),
335 };
336 let (span_default_event_name, span_event_name_mapping) = match spans {
337 Some(spans) => (spans.default_event_name, spans.event_name_mapping),
338 None => (None, None),
339 };
340
341 let log_table_name: Arc<str> = log_default_event_name.as_deref().unwrap_or("Log").into();
342 let span_table_name: Arc<str> = span_default_event_name.as_deref().unwrap_or("Span").into();
343
344 match log_event_name_mapping.as_ref() {
345 Some(mapping) => {
346 let routing_key_desc = match &mapping.routing_key {
347 LogsEventNameRoutingKey::EventName => "event_name".to_string(),
348 LogsEventNameRoutingKey::ResourceAttribute(attr) => {
349 format!("resource_attribute({})", attr)
350 }
351 LogsEventNameRoutingKey::ScopeAttribute(attr) => {
352 format!("scope_attribute({})", attr)
353 }
354 LogsEventNameRoutingKey::LogRecordAttribute(attr) => {
355 format!("log_record_attribute({})", attr)
356 }
357 };
358 info!(
359 name: "client.new.logs_config",
360 target: "geneva-uploader",
361 default_event_name = %log_default_event_name.as_deref().unwrap_or("<none>"),
362 routing_key = %routing_key_desc,
363 event_mapping_count = mapping.events.len(),
364 "Configured logs event name routing"
365 );
366 }
367 None if logs_present => {
368 info!(
369 name: "client.new.logs_config",
370 target: "geneva-uploader",
371 default_event_name = %log_default_event_name.as_deref().unwrap_or("<none>"),
372 "Configured logs event name routing"
373 );
374 }
375 None => {
376 info!(
377 name: "client.new.logs_config",
378 target: "geneva-uploader",
379 "Logs config not initialized; using default values for log events"
380 );
381 }
382 }
383
384 match span_event_name_mapping.as_ref() {
385 Some(mapping) => {
386 let routing_key_desc = match &mapping.routing_key {
387 SpanEventNameRoutingKey::ResourceAttribute(attr) => {
388 format!("resource_attribute({})", attr)
389 }
390 SpanEventNameRoutingKey::ScopeAttribute(attr) => {
391 format!("scope_attribute({})", attr)
392 }
393 SpanEventNameRoutingKey::SpanAttribute(attr) => {
394 format!("span_attribute({})", attr)
395 }
396 };
397 info!(
398 name: "client.new.spans_config",
399 target: "geneva-uploader",
400 default_event_name = %span_default_event_name.as_deref().unwrap_or("<none>"),
401 routing_key = %routing_key_desc,
402 event_mapping_count = mapping.events.len(),
403 "Configured spans event name routing"
404 );
405 }
406 None if spans_present => {
407 info!(
408 name: "client.new.spans_config",
409 target: "geneva-uploader",
410 default_event_name = %span_default_event_name.as_deref().unwrap_or("<none>"),
411 "Configured spans event name routing"
412 );
413 }
414 None => {
415 info!(
416 name: "client.new.spans_config",
417 target: "geneva-uploader",
418 "Spans config not initialized; using default values for span events"
419 );
420 }
421 }
422
423 let source_identity = format!(
424 "Tenant={}/Role={}/RoleInstance={}",
425 tenant, role_name, role_instance
426 );
427 let config_version = format!("Ver{}v0", config_major_version);
428
429 let metadata_fields = MetadataFields::new(
432 tenant,
433 role_name,
434 role_instance,
435 namespace.clone(),
436 config_version.clone(),
437 );
438
439 let uploader_config = GenevaUploaderConfig {
440 namespace,
441 source_identity,
442 environment,
443 config_version,
444 };
445
446 (
447 uploader_config,
448 ClientParts {
449 log_table_name,
450 log_event_name_mapping,
451 span_table_name,
452 span_event_name_mapping,
453 metadata_fields,
454 obo_event_map,
455 account_routing,
456 },
457 )
458 }
459
460 fn assemble(uploader: GenevaUploader, parts: ClientParts) -> Self {
462 Self {
463 uploader: Arc::new(uploader),
464 encoder: OtlpEncoder::new(),
465 metadata_fields: parts.metadata_fields,
466 log_table_name: parts.log_table_name,
467 log_event_name_mapping: parts.log_event_name_mapping,
468 span_table_name: parts.span_table_name,
469 span_event_name_mapping: parts.span_event_name_mapping,
470 obo_event_map: parts.obo_event_map,
471 account_routing: parts.account_routing,
472 }
473 }
474
475 pub fn new(cfg: GenevaClientConfig) -> Result<Self, String> {
476 info!(
477 name: "client.new",
478 target: "geneva-uploader",
479 endpoint = %cfg.endpoint,
480 namespace = %cfg.namespace,
481 account = %cfg.account,
482 "Initializing GenevaClient"
483 );
484
485 Self::validate_event_name_mappings(&cfg)?;
486
487 match cfg.auth_method {
489 AuthMethod::SystemManagedIdentity
490 | AuthMethod::UserManagedIdentity { .. }
491 | AuthMethod::UserManagedIdentityByObjectId { .. }
492 | AuthMethod::UserManagedIdentityByResourceId { .. } => {
493 if cfg.msi_resource.is_none() {
494 debug!(
495 name: "client.new.validate_msi_resource",
496 target: "geneva-uploader",
497 "Validation failed: msi_resource must be provided for managed identity auth"
498 );
499 return Err(
500 "msi_resource must be provided for managed identity auth".to_string()
501 );
502 }
503 }
504 AuthMethod::Certificate { .. } => {}
505 AuthMethod::WorkloadIdentity { .. } => {}
506 #[cfg(feature = "mock_auth")]
507 AuthMethod::MockAuth => {}
508 }
509
510 let config_client_config = GenevaConfigClientConfig {
511 endpoint: cfg.endpoint.clone(),
512 environment: cfg.environment.clone(),
513 account: cfg.account.clone(),
514 namespace: cfg.namespace.clone(),
515 region: cfg.region.clone(),
516 config_major_version: cfg.config_major_version,
517 auth_method: cfg.auth_method.clone(),
518 msi_resource: cfg.msi_resource.clone(),
519 #[cfg(test)]
520 test_root_ca_pem: None,
521 };
522 let config_client =
523 Arc::new(GenevaConfigClient::new(config_client_config).map_err(|e| {
524 debug!(
525 name: "client.new.config_client_init",
526 target: "geneva-uploader",
527 error = %e,
528 "GenevaConfigClient init failed"
529 );
530 format!("GenevaConfigClient init failed: {e}")
531 })?);
532
533 let (uploader_config, parts) = Self::derive_parts(cfg);
534
535 let uploader =
536 GenevaUploader::from_config_client(config_client, uploader_config).map_err(|e| {
537 debug!(
538 name: "client.new.uploader_init",
539 target: "geneva-uploader",
540 error = %e,
541 "GenevaUploader init failed"
542 );
543 format!("GenevaUploader init failed: {e}")
544 })?;
545
546 info!(
547 name: "client.new.complete",
548 target: "geneva-uploader",
549 "GenevaClient initialized successfully"
550 );
551
552 Ok(Self::assemble(uploader, parts))
553 }
554
555 pub fn with_agent_fed_source(
561 cfg: GenevaClientConfig,
562 source: Arc<dyn AgentFedCredentialSource>,
563 ) -> Result<Self, String> {
564 info!(
565 name: "client.new.agent_fed",
566 target: "geneva-uploader",
567 namespace = %cfg.namespace,
568 account = %cfg.account,
569 "Initializing GenevaClient (agent-fed credential source)"
570 );
571
572 Self::validate_event_name_mappings(&cfg)?;
573
574 let (uploader_config, parts) = Self::derive_parts(cfg);
575
576 let uploader = GenevaUploader::from_agent_fed(source, uploader_config).map_err(|e| {
577 debug!(
578 name: "client.new.agent_fed.uploader_init",
579 target: "geneva-uploader",
580 error = %e,
581 "GenevaUploader (agent-fed) init failed"
582 );
583 format!("GenevaUploader init failed: {e}")
584 })?;
585
586 Ok(Self::assemble(uploader, parts))
587 }
588
589 pub fn encode_and_compress_logs<T: LogsDataView>(
622 &self,
623 view: &T,
624 ) -> Result<Vec<EncodedBatch>, String> {
625 debug!(
626 name: "client.encode_and_compress_logs",
627 target: "geneva-uploader",
628 "Encoding and compressing logs"
629 );
630
631 let mut batches = self
632 .encoder
633 .encode_logs_from_view(
634 view,
635 &self.metadata_fields,
636 self.log_table_name.as_ref(),
637 self.log_event_name_mapping.as_ref(),
638 )
639 .map_err(|e| {
640 debug!(
641 name: "client.encode_and_compress_logs.error",
642 target: "geneva-uploader",
643 error = %e,
644 "Logs compression failed"
645 );
646 format!("Compression failed: {e}")
647 })?;
648 self.assign_account_groups(&mut batches);
649 Ok(batches)
650 }
651
652 pub fn encode_and_compress_spans(
654 &self,
655 spans: &[ResourceSpans],
656 ) -> Result<Vec<EncodedBatch>, String> {
657 debug!(
658 name: "client.encode_and_compress_spans",
659 target: "geneva-uploader",
660 resource_spans_count = spans.len(),
661 "Encoding and compressing resource spans"
662 );
663
664 let Some(span_event_name_mapping) = self.span_event_name_mapping.as_ref() else {
668 let all_spans = spans
669 .iter()
670 .flat_map(|resource_span| &resource_span.scope_spans)
671 .flat_map(|scope_span| &scope_span.spans);
672 let mut batches = self
673 .encoder
674 .encode_span_batch(
675 all_spans,
676 &self.metadata_fields,
677 self.span_table_name.as_ref(),
678 )
679 .map_err(|e| {
680 debug!(
681 name: "client.encode_and_compress_spans.error",
682 target: "geneva-uploader",
683 error = %e,
684 event_name = %self.span_table_name,
685 "Span compression failed"
686 );
687 format!("Compression failed: {e}")
688 })?;
689 self.assign_account_groups(&mut batches);
690 return Ok(batches);
691 };
692
693 let mut routed_groups: Vec<(String, Vec<&Span>)> = Vec::new();
694 let mut group_index: std::collections::HashMap<String, usize> =
695 std::collections::HashMap::new();
696
697 for resource_span in spans {
698 let resource = resource_span.resource.as_ref();
699 for scope_span in &resource_span.scope_spans {
700 let scope = scope_span.scope.as_ref();
701 let scope_routing = resolve_span_scope_routing(
702 resource,
703 scope,
704 self.span_table_name.as_ref(),
705 Some(span_event_name_mapping),
706 );
707 for span in &scope_span.spans {
708 let event_name = resolve_span_event_name_in_scope(
709 &scope_routing,
710 span,
711 self.span_table_name.as_ref(),
712 );
713
714 match group_index.get(event_name.as_ref()) {
715 Some(&idx) => routed_groups[idx].1.push(span),
716 None => {
717 let event_name = event_name.into_owned();
718 group_index.insert(event_name.clone(), routed_groups.len());
719 routed_groups.push((event_name, vec![span]));
720 }
721 }
722 }
723 }
724 }
725
726 let mut batches = Vec::new();
727 for (event_name, group_spans) in routed_groups {
728 let encoded = self
729 .encoder
730 .encode_span_batch(group_spans, &self.metadata_fields, &event_name)
731 .map_err(|e| {
732 debug!(
733 name: "client.encode_and_compress_spans.error",
734 target: "geneva-uploader",
735 error = %e,
736 event_name = %event_name,
737 "Span compression failed"
738 );
739 format!("Compression failed: {e}")
740 })?;
741 batches.extend(encoded);
742 }
743
744 self.assign_account_groups(&mut batches);
745 Ok(batches)
746 }
747
748 fn assign_account_groups(&self, batches: &mut [EncodedBatch]) {
751 for batch in batches {
752 batch.account_group = self
753 .account_routing
754 .group_for_event(&batch.event_name)
755 .to_string();
756 }
757 }
758
759 pub async fn upload_batch(&self, batch: &EncodedBatch) -> Result<(), UploadError> {
762 debug!(
763 name: "client.upload_batch",
764 target: "geneva-uploader",
765 event_name = %batch.event_name,
766 size = batch.data.len(),
767 "Uploading batch"
768 );
769
770 let obo_config = lookup_obo_config(self.obo_event_map.as_ref(), &batch.event_name)
772 .filter(|c| c.is_active());
773
774 self.uploader
775 .upload(
776 batch.data.clone(),
777 &batch.event_name,
778 &batch.account_group,
779 &batch.metadata,
780 batch.row_count,
781 obo_config,
782 )
783 .await
784 .map(|_| {
785 debug!(
786 name: "client.upload_batch.success",
787 target: "geneva-uploader",
788 event_name = %batch.event_name,
789 "Successfully uploaded batch"
790 );
791 })
792 .map_err(|e| {
793 debug!(
794 name: "client.upload_batch.error",
795 target: "geneva-uploader",
796 event_name = %batch.event_name,
797 error = %e,
798 "Geneva upload failed"
799 );
800 match e {
801 GenevaUploaderError::UploadFailed {
802 status,
803 retry_after,
804 message,
805 } => UploadError::HttpStatus {
806 status,
807 retry_after,
808 message,
809 },
810 GenevaUploaderError::Http(msg) => UploadError::Transport(msg),
811 GenevaUploaderError::AccountGroupNotResolved {
812 requested,
813 available,
814 } => UploadError::AccountGroupNotResolved {
815 requested,
816 available,
817 },
818 other => UploadError::Other(other.to_string()),
819 }
820 })
821 }
822}
823
824#[cfg(test)]
825mod tests {
826 use super::*;
827 use opentelemetry_proto::tonic::collector::logs::v1::ExportLogsServiceRequest;
828 use opentelemetry_proto::tonic::logs::v1::{LogRecord, ResourceLogs, ScopeLogs};
829 use opentelemetry_proto::tonic::trace::v1::{ResourceSpans, ScopeSpans, Span};
830 use otap_df_pdata::views::otlp::bytes::logs::RawLogsData;
831 use prost::Message as _;
832 use std::collections::HashMap;
833
834 fn build_config(logs: Option<&str>, spans: Option<&str>) -> GenevaClientConfig {
835 GenevaClientConfig {
836 endpoint: "https://example.test".to_string(),
837 environment: "Test".to_string(),
838 account: "acct".to_string(),
839 namespace: "ns".to_string(),
840 account_routing: AccountRouting::new("test-group"),
841 region: "eastus".to_string(),
842 config_major_version: 2,
843 auth_method: AuthMethod::WorkloadIdentity {
844 resource: "https://monitor.azure.com".to_string(),
845 },
846 tenant: "tenant".to_string(),
847 role_name: "role".to_string(),
848 role_instance: "instance".to_string(),
849 msi_resource: None,
850 logs: logs.map(|default_event_name| LogsConfig {
851 default_event_name: Some(default_event_name.to_owned()),
852 event_name_mapping: None,
853 }),
854 spans: spans.map(|default_event_name| TracesConfig {
855 default_event_name: Some(default_event_name.to_owned()),
856 event_name_mapping: None,
857 }),
858 obo_event_map: None,
859 }
860 }
861
862 fn build_client(logs: Option<&str>, spans: Option<&str>) -> GenevaClient {
863 GenevaClient::new(build_config(logs, spans)).expect("client should initialize")
864 }
865
866 #[test]
869 fn derive_parts_preserves_transport_environment_and_version() {
870 let (uploader, metadata) = GenevaClient::derive_parts(build_config(None, None));
871
872 assert_eq!(uploader.environment, "Test");
873 assert_eq!(uploader.config_version, "Ver2v0");
874 assert_eq!(metadata.metadata_fields.event_version, "Ver2v0");
875 }
876
877 fn build_span_client(
878 default_event_name: Option<&str>,
879 mapping: Option<SpanEventNameMapping>,
880 ) -> GenevaClient {
881 let mut cfg = build_config(None, default_event_name);
882 cfg.spans = Some(TracesConfig {
883 default_event_name: default_event_name.map(str::to_owned),
884 event_name_mapping: mapping,
885 });
886 GenevaClient::new(cfg).expect("span client should initialize")
887 }
888
889 fn make_span_event_name_mapping(
890 routing_key: SpanEventNameRoutingKey,
891 events: &[(&str, Option<&str>)],
892 ) -> SpanEventNameMapping {
893 let mut map = HashMap::new();
894 for (source, destination) in events {
895 map.insert((*source).to_string(), destination.map(str::to_string));
896 }
897 SpanEventNameMapping {
898 routing_key,
899 events: map,
900 }
901 }
902
903 fn string_attr(key: &str, value: &str) -> KeyValue {
904 KeyValue {
905 key: key.to_string(),
906 key_strindex: 0,
907 value: Some(opentelemetry_proto::tonic::common::v1::AnyValue {
908 value: Some(
909 opentelemetry_proto::tonic::common::v1::any_value::Value::StringValue(
910 value.to_string(),
911 ),
912 ),
913 }),
914 }
915 }
916
917 fn int_attr(key: &str, value: i64) -> KeyValue {
918 KeyValue {
919 key: key.to_string(),
920 key_strindex: 0,
921 value: Some(opentelemetry_proto::tonic::common::v1::AnyValue {
922 value: Some(
923 opentelemetry_proto::tonic::common::v1::any_value::Value::IntValue(value),
924 ),
925 }),
926 }
927 }
928
929 fn double_attr(key: &str, value: f64) -> KeyValue {
930 KeyValue {
931 key: key.to_string(),
932 key_strindex: 0,
933 value: Some(opentelemetry_proto::tonic::common::v1::AnyValue {
934 value: Some(
935 opentelemetry_proto::tonic::common::v1::any_value::Value::DoubleValue(value),
936 ),
937 }),
938 }
939 }
940
941 fn bool_attr(key: &str, value: bool) -> KeyValue {
942 KeyValue {
943 key: key.to_string(),
944 key_strindex: 0,
945 value: Some(opentelemetry_proto::tonic::common::v1::AnyValue {
946 value: Some(
947 opentelemetry_proto::tonic::common::v1::any_value::Value::BoolValue(value),
948 ),
949 }),
950 }
951 }
952
953 fn bytes_attr(key: &str, value: Vec<u8>) -> KeyValue {
954 KeyValue {
955 key: key.to_string(),
956 key_strindex: 0,
957 value: Some(opentelemetry_proto::tonic::common::v1::AnyValue {
958 value: Some(
959 opentelemetry_proto::tonic::common::v1::any_value::Value::BytesValue(value),
960 ),
961 }),
962 }
963 }
964
965 fn build_span_request(
966 resource_attrs: Vec<KeyValue>,
967 scope_name: Option<&str>,
968 scope_attrs: Vec<KeyValue>,
969 span_attrs: Vec<KeyValue>,
970 ) -> Vec<ResourceSpans> {
971 vec![ResourceSpans {
972 resource: (!resource_attrs.is_empty()).then_some(Resource {
973 attributes: resource_attrs,
974 ..Default::default()
975 }),
976 scope_spans: vec![ScopeSpans {
977 scope: (scope_name.is_some() || !scope_attrs.is_empty()).then_some(
978 InstrumentationScope {
979 name: scope_name.unwrap_or_default().to_string(),
980 attributes: scope_attrs,
981 ..Default::default()
982 },
983 ),
984 spans: vec![Span {
985 attributes: span_attrs,
986 ..Default::default()
987 }],
988 ..Default::default()
989 }],
990 ..Default::default()
991 }]
992 }
993
994 #[test]
995 fn new_rejects_logs_mapping_with_empty_events() {
996 let mut cfg = build_config(None, None);
997 cfg.logs = Some(LogsConfig {
998 default_event_name: None,
999 event_name_mapping: Some(LogsEventNameMapping {
1000 routing_key: LogsEventNameRoutingKey::EventName,
1001 events: HashMap::new(),
1002 }),
1003 });
1004
1005 let err = match GenevaClient::new(cfg) {
1006 Ok(_) => panic!("empty mapping events must be rejected"),
1007 Err(err) => err,
1008 };
1009 assert!(err.contains("events must be non-empty"));
1010 }
1011
1012 #[test]
1013 fn new_rejects_invalid_account_routing() {
1014 let mut cfg = build_config(None, None);
1015 cfg.account_routing = AccountRouting::new(" ");
1016
1017 let err = match GenevaClient::new(cfg) {
1018 Ok(_) => panic!("blank default account group must be rejected"),
1019 Err(err) => err,
1020 };
1021 assert!(err.contains("default_group must not be empty"));
1022
1023 let mut cfg = build_config(None, None);
1024 cfg.account_routing = AccountRouting::new("default").with_event_group("Log", " ");
1025
1026 let err = match GenevaClient::new(cfg) {
1027 Ok(_) => panic!("blank mapped account group must be rejected"),
1028 Err(err) => err,
1029 };
1030 assert!(err.contains("event and group names must not be empty"));
1031 }
1032
1033 #[test]
1034 fn account_routing_bulk_builder_extends_event_groups() {
1035 let routing = AccountRouting::new("default").with_event_groups(HashMap::from([
1036 ("SecurityEvent".to_string(), "security".to_string()),
1037 ("AuditEvent".to_string(), "audit".to_string()),
1038 ]));
1039
1040 assert_eq!(routing.group_for_event("SecurityEvent"), "security");
1041 assert_eq!(routing.group_for_event("AuditEvent"), "audit");
1042 assert_eq!(routing.group_for_event("UnmappedEvent"), "default");
1043 }
1044
1045 #[test]
1046 fn encoded_batches_route_final_event_name_to_account_group() {
1047 let mut cfg = build_config(Some("SecurityEvent"), None);
1048 cfg.account_routing =
1049 AccountRouting::new("diagnostics").with_event_group("SecurityEvent", "security");
1050 let client = GenevaClient::new(cfg).expect("client should initialize");
1051
1052 let request = ExportLogsServiceRequest {
1053 resource_logs: vec![ResourceLogs {
1054 scope_logs: vec![ScopeLogs {
1055 log_records: vec![LogRecord::default()],
1056 ..Default::default()
1057 }],
1058 ..Default::default()
1059 }],
1060 };
1061 let bytes = request.encode_to_vec();
1062 let batches = client
1063 .encode_and_compress_logs(&RawLogsData::new(&bytes))
1064 .expect("log encoding should succeed");
1065
1066 assert_eq!(batches.len(), 1);
1067 assert_eq!(batches[0].event_name, "SecurityEvent");
1068 assert_eq!(batches[0].account_group, "security");
1069 }
1070
1071 #[test]
1072 fn new_rejects_spans_mapping_with_empty_events() {
1073 let mut cfg = build_config(None, None);
1074 cfg.spans = Some(TracesConfig {
1075 default_event_name: None,
1076 event_name_mapping: Some(SpanEventNameMapping {
1077 routing_key: SpanEventNameRoutingKey::SpanAttribute("cluster".to_string()),
1078 events: HashMap::new(),
1079 }),
1080 });
1081
1082 let err = match GenevaClient::new(cfg) {
1083 Ok(_) => panic!("empty span mapping events must be rejected"),
1084 Err(err) => err,
1085 };
1086 assert!(err.contains("events must be non-empty"));
1087 }
1088
1089 #[test]
1090 fn new_rejects_logs_mapping_with_blank_source_key() {
1091 let mut cfg = build_config(None, None);
1092 cfg.logs = Some(LogsConfig {
1093 default_event_name: None,
1094 event_name_mapping: Some(LogsEventNameMapping {
1095 routing_key: LogsEventNameRoutingKey::EventName,
1096 events: HashMap::from([(" ".to_string(), Some("TableA".to_string()))]),
1097 }),
1098 });
1099
1100 let err = match GenevaClient::new(cfg) {
1101 Ok(_) => panic!("blank source key must be rejected"),
1102 Err(err) => err,
1103 };
1104 assert!(err.contains("source keys must not be blank"));
1105 }
1106
1107 #[test]
1108 fn new_rejects_spans_mapping_with_blank_routing_key_name() {
1109 let mut cfg = build_config(None, None);
1110 cfg.spans = Some(TracesConfig {
1111 default_event_name: None,
1112 event_name_mapping: Some(SpanEventNameMapping {
1113 routing_key: SpanEventNameRoutingKey::SpanAttribute(" ".to_string()),
1114 events: HashMap::from([("cluster-a".to_string(), Some("TraceA".to_string()))]),
1115 }),
1116 });
1117
1118 let err = match GenevaClient::new(cfg) {
1119 Ok(_) => panic!("blank routing key name must be rejected"),
1120 Err(err) => err,
1121 };
1122 assert!(err.contains("attribute name must not be blank"));
1123 }
1124
1125 #[test]
1126 fn default_event_name_unwrap_or_prefers_override_and_falls_back() {
1127 let configured = maybe_event_name(true);
1128 let missing = maybe_event_name(false);
1129
1130 assert_eq!(configured.unwrap_or("Log"), "AppLog");
1131 assert_eq!(missing.unwrap_or("Log"), "Log");
1132 }
1133
1134 fn maybe_event_name(configured: bool) -> Option<&'static str> {
1135 if configured {
1136 Some("AppLog")
1137 } else {
1138 None
1139 }
1140 }
1141
1142 #[test]
1143 fn encode_and_compress_logs_uses_configured_default_event_name() {
1144 let client = build_client(Some("AppLog"), None);
1145
1146 let request = ExportLogsServiceRequest {
1147 resource_logs: vec![ResourceLogs {
1148 scope_logs: vec![ScopeLogs {
1149 log_records: vec![LogRecord::default()],
1150 ..Default::default()
1151 }],
1152 ..Default::default()
1153 }],
1154 };
1155
1156 let bytes = request.encode_to_vec();
1157 let view = RawLogsData::new(&bytes);
1158 let batches = client
1159 .encode_and_compress_logs(&view)
1160 .expect("log encoding should succeed");
1161
1162 assert_eq!(batches.len(), 1);
1163 assert_eq!(batches[0].event_name, "AppLog");
1164 }
1165
1166 #[test]
1167 fn encode_and_compress_spans_uses_configured_default_event_name() {
1168 let client = build_client(None, Some("AppTrace"));
1169
1170 let spans = vec![ResourceSpans {
1171 scope_spans: vec![ScopeSpans {
1172 spans: vec![Span {
1173 name: "span-name".to_string(),
1174 ..Default::default()
1175 }],
1176 ..Default::default()
1177 }],
1178 ..Default::default()
1179 }];
1180
1181 let batches = client
1182 .encode_and_compress_spans(&spans)
1183 .expect("span encoding should succeed");
1184
1185 assert_eq!(batches.len(), 1);
1186 assert_eq!(batches[0].event_name, "AppTrace");
1187 }
1188
1189 #[test]
1190 fn encode_and_compress_spans_no_mapping_shares_default_name_across_spans() {
1191 let client = build_client(None, Some("AppTrace"));
1195
1196 let spans = vec![ResourceSpans {
1197 scope_spans: vec![ScopeSpans {
1198 spans: vec![
1199 Span {
1200 name: "span-1".to_string(),
1201 ..Default::default()
1202 },
1203 Span {
1204 name: "span-2".to_string(),
1205 ..Default::default()
1206 },
1207 Span {
1208 name: "span-3".to_string(),
1209 ..Default::default()
1210 },
1211 ],
1212 ..Default::default()
1213 }],
1214 ..Default::default()
1215 }];
1216
1217 let batches = client
1218 .encode_and_compress_spans(&spans)
1219 .expect("span encoding should succeed");
1220
1221 assert_eq!(batches.len(), 1);
1222 assert_eq!(batches[0].event_name, "AppTrace");
1223 assert_eq!(batches[0].row_count, 3);
1224 }
1225
1226 #[test]
1227 fn encode_and_compress_spans_routes_by_resource_scope_and_span_attributes() {
1228 let client = build_span_client(
1229 Some("AppTrace"),
1230 Some(make_span_event_name_mapping(
1231 SpanEventNameRoutingKey::ResourceAttribute("cluster".to_string()),
1232 &[("cluster-a", Some("ResourceTrace"))],
1233 )),
1234 );
1235
1236 let resource_spans = build_span_request(
1237 vec![string_attr("cluster", "cluster-a")],
1238 Some("scope-a"),
1239 Vec::new(),
1240 Vec::new(),
1241 );
1242
1243 let batches = client
1244 .encode_and_compress_spans(&resource_spans)
1245 .expect("span encoding should succeed");
1246
1247 assert_eq!(batches.len(), 1);
1248 assert_eq!(batches[0].event_name, "ResourceTrace");
1249 }
1250
1251 #[test]
1252 fn encode_and_compress_spans_resource_route_shared_across_spans_in_scope() {
1253 let client = build_span_client(
1257 Some("AppTrace"),
1258 Some(make_span_event_name_mapping(
1259 SpanEventNameRoutingKey::ResourceAttribute("cluster".to_string()),
1260 &[("cluster-a", Some("ResourceTrace"))],
1261 )),
1262 );
1263
1264 let resource_spans = vec![ResourceSpans {
1265 resource: Some(Resource {
1266 attributes: vec![string_attr("cluster", "cluster-a")],
1267 ..Default::default()
1268 }),
1269 scope_spans: vec![ScopeSpans {
1270 scope: Some(InstrumentationScope {
1271 name: "scope-a".to_string(),
1272 ..Default::default()
1273 }),
1274 spans: vec![
1275 Span {
1276 name: "span-1".to_string(),
1277 ..Default::default()
1278 },
1279 Span {
1280 name: "span-2".to_string(),
1281 ..Default::default()
1282 },
1283 ],
1284 ..Default::default()
1285 }],
1286 ..Default::default()
1287 }];
1288
1289 let batches = client
1290 .encode_and_compress_spans(&resource_spans)
1291 .expect("span encoding should succeed");
1292
1293 assert_eq!(batches.len(), 1);
1294 assert_eq!(batches[0].event_name, "ResourceTrace");
1295 assert_eq!(batches[0].row_count, 2);
1296 }
1297
1298 #[test]
1299 fn encode_and_compress_spans_routes_by_scope_attribute() {
1300 let client = build_span_client(
1301 Some("AppTrace"),
1302 Some(make_span_event_name_mapping(
1303 SpanEventNameRoutingKey::ScopeAttribute("cluster".to_string()),
1304 &[("scope-a", Some("ScopeTrace"))],
1305 )),
1306 );
1307
1308 let resource_spans = build_span_request(
1309 Vec::new(),
1310 Some("scope-a"),
1311 vec![string_attr("cluster", "scope-a")],
1312 Vec::new(),
1313 );
1314
1315 let batches = client
1316 .encode_and_compress_spans(&resource_spans)
1317 .expect("span encoding should succeed");
1318
1319 assert_eq!(batches.len(), 1);
1320 assert_eq!(batches[0].event_name, "ScopeTrace");
1321 }
1322
1323 #[test]
1324 fn encode_and_compress_spans_routes_by_scope_name_with_passthrough_destination() {
1325 let client = build_span_client(
1326 Some("AppTrace"),
1327 Some(make_span_event_name_mapping(
1328 SpanEventNameRoutingKey::ScopeAttribute("scope.name".to_string()),
1329 &[("scope-a", Some(""))],
1330 )),
1331 );
1332
1333 let resource_spans =
1334 build_span_request(Vec::new(), Some("scope-a"), Vec::new(), Vec::new());
1335
1336 let batches = client
1337 .encode_and_compress_spans(&resource_spans)
1338 .expect("span encoding should succeed");
1339
1340 assert_eq!(batches.len(), 1);
1341 assert_eq!(batches[0].event_name, "scope-a");
1342 }
1343
1344 #[test]
1345 fn encode_and_compress_spans_routes_by_span_attribute_and_splits_batches() {
1346 let client = build_span_client(
1347 Some("AppTrace"),
1348 Some(make_span_event_name_mapping(
1349 SpanEventNameRoutingKey::SpanAttribute("cluster".to_string()),
1350 &[
1351 ("cluster-a", Some("SpanTraceA")),
1352 ("cluster-b", Some("SpanTraceB")),
1353 ],
1354 )),
1355 );
1356
1357 let resource_spans = vec![ResourceSpans {
1358 scope_spans: vec![ScopeSpans {
1359 scope: Some(InstrumentationScope {
1360 name: "scope-a".to_string(),
1361 ..Default::default()
1362 }),
1363 spans: vec![
1364 Span {
1365 attributes: vec![string_attr("cluster", "cluster-a")],
1366 ..Default::default()
1367 },
1368 Span {
1369 attributes: vec![string_attr("cluster", "cluster-b")],
1370 ..Default::default()
1371 },
1372 ],
1373 ..Default::default()
1374 }],
1375 ..Default::default()
1376 }];
1377
1378 let batches = client
1379 .encode_and_compress_spans(&resource_spans)
1380 .expect("span encoding should succeed");
1381
1382 let mut event_names: Vec<String> = batches
1383 .iter()
1384 .map(|batch| batch.event_name.clone())
1385 .collect();
1386 event_names.sort();
1387 assert_eq!(
1388 event_names,
1389 vec!["SpanTraceA".to_string(), "SpanTraceB".to_string()]
1390 );
1391 }
1392
1393 #[test]
1394 fn encode_and_compress_spans_missing_mapping_key_falls_back_to_default_event_name() {
1395 let client = build_span_client(
1396 Some("AppTrace"),
1397 Some(make_span_event_name_mapping(
1398 SpanEventNameRoutingKey::SpanAttribute("cluster".to_string()),
1399 &[("known", Some("SpanTrace"))],
1400 )),
1401 );
1402
1403 let resource_spans = build_span_request(
1404 Vec::new(),
1405 Some("scope-a"),
1406 Vec::new(),
1407 vec![string_attr("cluster", "unknown")],
1408 );
1409
1410 let batches = client
1411 .encode_and_compress_spans(&resource_spans)
1412 .expect("span encoding should succeed");
1413
1414 assert_eq!(batches.len(), 1);
1415 assert_eq!(batches[0].event_name, "AppTrace");
1416 }
1417
1418 #[test]
1419 fn encode_and_compress_spans_routes_by_non_string_attribute_value() {
1420 let client = build_span_client(
1422 Some("AppTrace"),
1423 Some(make_span_event_name_mapping(
1424 SpanEventNameRoutingKey::SpanAttribute("code".to_string()),
1425 &[("42", Some("IntTrace"))],
1426 )),
1427 );
1428
1429 let resource_spans = build_span_request(
1430 Vec::new(),
1431 Some("scope-a"),
1432 Vec::new(),
1433 vec![int_attr("code", 42)],
1434 );
1435
1436 let batches = client
1437 .encode_and_compress_spans(&resource_spans)
1438 .expect("span encoding should succeed");
1439
1440 assert_eq!(batches.len(), 1);
1441 assert_eq!(batches[0].event_name, "IntTrace");
1442 }
1443
1444 #[test]
1445 fn span_routing_value_from_attributes_handles_all_value_types() {
1446 let attrs = vec![
1447 string_attr("s", "sv"),
1448 int_attr("i", 7),
1449 double_attr("d", 2.5),
1450 bool_attr("b", true),
1451 bytes_attr("raw", b"xyz".to_vec()),
1452 ];
1453
1454 assert_eq!(
1455 span_routing_value_from_attributes(&attrs, "s").as_deref(),
1456 Some("sv")
1457 );
1458 assert_eq!(
1459 span_routing_value_from_attributes(&attrs, "i").as_deref(),
1460 Some("7")
1461 );
1462 assert_eq!(
1463 span_routing_value_from_attributes(&attrs, "d").as_deref(),
1464 Some("2.5")
1465 );
1466 assert_eq!(
1467 span_routing_value_from_attributes(&attrs, "b").as_deref(),
1468 Some("true")
1469 );
1470 assert_eq!(span_routing_value_from_attributes(&attrs, "raw"), None);
1472 assert_eq!(span_routing_value_from_attributes(&attrs, "absent"), None);
1473 let blank = vec![string_attr("s", " ")];
1475 assert_eq!(span_routing_value_from_attributes(&blank, "s"), None);
1476 }
1477
1478 #[test]
1479 fn span_routing_value_from_scope_reads_name_version_and_attributes() {
1480 let scope = InstrumentationScope {
1481 name: "scope-a".to_string(),
1482 version: "1.2.3".to_string(),
1483 attributes: vec![string_attr("cluster", "clusterA")],
1484 ..Default::default()
1485 };
1486
1487 assert_eq!(
1488 span_routing_value_from_scope(Some(&scope), SCOPE_NAME_ROUTING_KEY).as_deref(),
1489 Some("scope-a")
1490 );
1491 assert_eq!(
1492 span_routing_value_from_scope(Some(&scope), SCOPE_VERSION_ROUTING_KEY).as_deref(),
1493 Some("1.2.3")
1494 );
1495 assert_eq!(
1496 span_routing_value_from_scope(Some(&scope), "cluster").as_deref(),
1497 Some("clusterA")
1498 );
1499 assert_eq!(span_routing_value_from_scope(Some(&scope), "absent"), None);
1500 assert_eq!(
1502 span_routing_value_from_scope(None, SCOPE_NAME_ROUTING_KEY),
1503 None
1504 );
1505 let blank = InstrumentationScope {
1507 name: " ".to_string(),
1508 version: String::new(),
1509 ..Default::default()
1510 };
1511 assert_eq!(
1512 span_routing_value_from_scope(Some(&blank), SCOPE_NAME_ROUTING_KEY),
1513 None
1514 );
1515 assert_eq!(
1516 span_routing_value_from_scope(Some(&blank), SCOPE_VERSION_ROUTING_KEY),
1517 None
1518 );
1519 }
1520
1521 #[test]
1522 fn span_routing_value_from_resource_reads_attributes_or_none() {
1523 let resource = Resource {
1524 attributes: vec![string_attr("region", "eastus")],
1525 ..Default::default()
1526 };
1527
1528 assert_eq!(
1529 span_routing_value_from_resource(Some(&resource), "region").as_deref(),
1530 Some("eastus")
1531 );
1532 assert_eq!(
1533 span_routing_value_from_resource(Some(&resource), "absent"),
1534 None
1535 );
1536 assert_eq!(span_routing_value_from_resource(None, "region"), None);
1538 }
1539
1540 #[test]
1541 fn resolve_span_event_name_falls_back_and_passes_through() {
1542 let span = Span {
1543 attributes: vec![string_attr("cluster", "clusterA")],
1544 ..Default::default()
1545 };
1546
1547 assert_eq!(
1549 resolve_span_event_name(None, None, &span, "Span", None),
1550 "Span"
1551 );
1552
1553 let mapping = make_span_event_name_mapping(
1554 SpanEventNameRoutingKey::SpanAttribute("cluster".to_string()),
1555 &[("clusterA", Some("")), ("clusterB", Some("Premium"))],
1556 );
1557
1558 assert_eq!(
1560 resolve_span_event_name(None, None, &span, "Span", Some(&mapping)),
1561 "clusterA"
1562 );
1563
1564 let other = Span {
1566 attributes: vec![string_attr("region", "eastus")],
1567 ..Default::default()
1568 };
1569 assert_eq!(
1570 resolve_span_event_name(None, None, &other, "Span", Some(&mapping)),
1571 "Span"
1572 );
1573 }
1574
1575 #[test]
1576 fn new_logs_client_accepts_all_attribute_routing_kinds() {
1577 for routing_key in [
1580 LogsEventNameRoutingKey::ResourceAttribute("res".to_string()),
1581 LogsEventNameRoutingKey::ScopeAttribute("scope".to_string()),
1582 LogsEventNameRoutingKey::LogRecordAttribute("rec".to_string()),
1583 ] {
1584 let mut cfg = build_config(Some("AppLog"), None);
1585 cfg.logs = Some(LogsConfig {
1586 default_event_name: Some("AppLog".to_string()),
1587 event_name_mapping: Some(LogsEventNameMapping {
1588 routing_key,
1589 events: HashMap::from([("src".to_string(), Some("Dest".to_string()))]),
1590 }),
1591 });
1592 GenevaClient::new(cfg).expect("logs client with attribute routing should initialize");
1593 }
1594 }
1595
1596 #[test]
1597 fn encode_and_compress_spans_groups_multiple_spans_with_same_route() {
1598 let client = build_span_client(
1601 Some("AppTrace"),
1602 Some(make_span_event_name_mapping(
1603 SpanEventNameRoutingKey::SpanAttribute("cluster".to_string()),
1604 &[("clusterA", Some("TraceA"))],
1605 )),
1606 );
1607
1608 let resource_spans = vec![ResourceSpans {
1609 resource: None,
1610 scope_spans: vec![ScopeSpans {
1611 scope: Some(InstrumentationScope {
1612 name: "s".to_string(),
1613 ..Default::default()
1614 }),
1615 spans: vec![
1616 Span {
1617 attributes: vec![string_attr("cluster", "clusterA")],
1618 ..Default::default()
1619 },
1620 Span {
1621 attributes: vec![string_attr("cluster", "clusterA")],
1622 ..Default::default()
1623 },
1624 ],
1625 ..Default::default()
1626 }],
1627 ..Default::default()
1628 }];
1629
1630 let batches = client
1631 .encode_and_compress_spans(&resource_spans)
1632 .expect("span encoding should succeed");
1633
1634 assert_eq!(batches.len(), 1);
1635 assert_eq!(batches[0].event_name, "TraceA");
1636 assert_eq!(batches[0].row_count, 2);
1637 }
1638
1639 #[test]
1640 fn encode_and_compress_logs_uses_default_table_name_when_logs_config_absent() {
1641 let client = GenevaClient::new(GenevaClientConfig {
1642 endpoint: "https://example.test".to_string(),
1643 environment: "Test".to_string(),
1644 account: "acct".to_string(),
1645 namespace: "ns".to_string(),
1646 account_routing: AccountRouting::new("test-group"),
1647 region: "eastus".to_string(),
1648 config_major_version: 2,
1649 auth_method: AuthMethod::WorkloadIdentity {
1650 resource: "https://monitor.azure.com".to_string(),
1651 },
1652 tenant: "tenant".to_string(),
1653 role_name: "role".to_string(),
1654 role_instance: "instance".to_string(),
1655 msi_resource: None,
1656 logs: None,
1657 spans: None,
1658 obo_event_map: None,
1659 })
1660 .expect("client should initialize without optional log config");
1661
1662 let request = ExportLogsServiceRequest {
1663 resource_logs: vec![ResourceLogs {
1664 scope_logs: vec![ScopeLogs {
1665 log_records: vec![LogRecord::default()],
1666 ..Default::default()
1667 }],
1668 ..Default::default()
1669 }],
1670 };
1671 let bytes = request.encode_to_vec();
1672 let view = RawLogsData::new(&bytes);
1673 let batches = client
1674 .encode_and_compress_logs(&view)
1675 .expect("log encoding should succeed");
1676
1677 assert_eq!(batches.len(), 1);
1678 assert_eq!(batches[0].event_name, "Log");
1679 }
1680
1681 #[test]
1682 fn encode_and_compress_spans_uses_default_table_name_when_spans_config_absent() {
1683 let client = GenevaClient::new(GenevaClientConfig {
1684 endpoint: "https://example.test".to_string(),
1685 environment: "Test".to_string(),
1686 account: "acct".to_string(),
1687 namespace: "ns".to_string(),
1688 account_routing: AccountRouting::new("test-group"),
1689 region: "eastus".to_string(),
1690 config_major_version: 2,
1691 auth_method: AuthMethod::WorkloadIdentity {
1692 resource: "https://monitor.azure.com".to_string(),
1693 },
1694 tenant: "tenant".to_string(),
1695 role_name: "role".to_string(),
1696 role_instance: "instance".to_string(),
1697 msi_resource: None,
1698 logs: None,
1699 spans: None,
1700 obo_event_map: None,
1701 })
1702 .expect("client should initialize without optional span config");
1703
1704 let spans = vec![ResourceSpans {
1705 scope_spans: vec![ScopeSpans {
1706 spans: vec![Span {
1707 name: "span-name".to_string(),
1708 ..Default::default()
1709 }],
1710 ..Default::default()
1711 }],
1712 ..Default::default()
1713 }];
1714
1715 let batches = client
1716 .encode_and_compress_spans(&spans)
1717 .expect("span encoding should succeed");
1718
1719 assert_eq!(batches.len(), 1);
1720 assert_eq!(batches[0].event_name, "Span");
1721 }
1722
1723 #[test]
1726 fn with_agent_fed_source_builds_usable_client() {
1727 #[derive(Debug)]
1728 struct StubSource;
1729 impl AgentFedCredentialSource for StubSource {
1730 fn current(&self) -> AgentFedCredentialFuture<'_> {
1731 Box::pin(async {
1732 Some(AgentFedCredential::new(
1733 "secret-token",
1734 "https://ingest.example",
1735 HashMap::from([("test-group".to_string(), "moniker".to_string())]),
1736 ))
1737 })
1738 }
1739 }
1740
1741 let client = GenevaClient::with_agent_fed_source(
1742 build_config(Some("AppLog"), None),
1743 Arc::new(StubSource),
1744 )
1745 .expect("agent-fed client should initialize");
1746
1747 let request = ExportLogsServiceRequest {
1750 resource_logs: vec![ResourceLogs {
1751 scope_logs: vec![ScopeLogs {
1752 log_records: vec![LogRecord::default()],
1753 ..Default::default()
1754 }],
1755 ..Default::default()
1756 }],
1757 };
1758 let bytes = request.encode_to_vec();
1759 let view = RawLogsData::new(&bytes);
1760 let batches = client
1761 .encode_and_compress_logs(&view)
1762 .expect("log encoding should succeed");
1763 assert_eq!(batches.len(), 1);
1764 assert_eq!(batches[0].event_name, "AppLog");
1765 }
1766
1767 #[test]
1770 fn agent_fed_credential_debug_redacts_token() {
1771 let cred = AgentFedCredential::new(
1772 "top-secret",
1773 "https://ingest.example",
1774 HashMap::from([("test-group".to_string(), "moniker".to_string())]),
1775 );
1776 assert_eq!(cred.expose_token(), "top-secret");
1777 let rendered = format!("{cred:?}");
1778 assert!(
1779 !rendered.contains("top-secret"),
1780 "token must be redacted: {rendered}"
1781 );
1782 assert!(rendered.contains("<redacted>"));
1783 assert!(rendered.contains("test-group"));
1784 }
1785}