1#![allow(unsafe_attr_outside_unsafe)]
5
6use std::ffi::CStr;
7use std::os::raw::{c_char, c_uint};
8use std::ptr;
9use std::sync::OnceLock;
10use tokio::runtime::Runtime;
11
12use geneva_uploader::client::{EncodedBatch, GenevaClient, GenevaClientConfig};
13use geneva_uploader::AuthMethod;
14use opentelemetry_proto::tonic::collector::logs::v1::ExportLogsServiceRequest;
15use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceRequest;
16use prost::Message;
17use std::path::PathBuf;
18
19const GENEVA_HANDLE_MAGIC: u64 = 0xFEED_BEEF;
21
22static RUNTIME: OnceLock<Runtime> = OnceLock::new(); fn runtime() -> &'static Runtime {
32 RUNTIME.get_or_init(|| {
33 tokio::runtime::Builder::new_multi_thread()
34 .worker_threads(
35 std::thread::available_parallelism()
36 .map(|n| n.get())
37 .unwrap_or(4),
38 )
39 .thread_name("geneva-ffi-worker")
40 .enable_time()
41 .enable_io() .build()
43 .expect("Failed to create Tokio runtime for Geneva FFI")
44 })
45}
46
47trait ValidatedHandle {
49 fn magic(&self) -> u64;
50 fn set_magic(&mut self, magic: u64);
51}
52
53unsafe fn validate_handle<T: ValidatedHandle>(handle: *const T) -> GenevaError {
55 if handle.is_null() {
56 return GenevaError::NullPointer;
57 }
58
59 let handle_ref = unsafe { handle.as_ref().unwrap() };
60
61 if handle_ref.magic() != GENEVA_HANDLE_MAGIC {
62 return GenevaError::InvalidHandle;
63 }
64
65 GenevaError::Success
66}
67
68unsafe fn clear_handle_magic<T: ValidatedHandle>(handle: *mut T) {
70 if !handle.is_null() {
71 if let Some(h) = unsafe { handle.as_mut() } {
72 h.set_magic(0);
73 }
74 }
75}
76
77pub struct GenevaClientHandle {
79 magic: u64, client: GenevaClient,
81}
82
83impl ValidatedHandle for GenevaClientHandle {
84 fn magic(&self) -> u64 {
85 self.magic
86 }
87
88 fn set_magic(&mut self, magic: u64) {
89 self.magic = magic;
90 }
91}
92
93pub struct EncodedBatchesHandle {
95 magic: u64,
96 batches: Vec<EncodedBatch>,
97}
98
99impl ValidatedHandle for EncodedBatchesHandle {
100 fn magic(&self) -> u64 {
101 self.magic
102 }
103
104 fn set_magic(&mut self, magic: u64) {
105 self.magic = magic;
106 }
107}
108
109#[repr(C)]
111#[derive(Copy, Clone)]
112pub struct GenevaCertAuthConfig {
113 pub cert_path: *const c_char, pub cert_password: *const c_char, }
116
117#[repr(C)]
119#[derive(Copy, Clone)]
120pub struct GenevaWorkloadIdentityAuthConfig {
121 pub resource: *const c_char, }
123
124#[repr(C)]
126#[derive(Copy, Clone)]
127pub struct GenevaUserManagedIdentityAuthConfig {
128 pub client_id: *const c_char, }
130
131#[repr(C)]
133#[derive(Copy, Clone)]
134pub struct GenevaUserManagedIdentityByObjectIdAuthConfig {
135 pub object_id: *const c_char, }
137
138#[repr(C)]
140#[derive(Copy, Clone)]
141pub struct GenevaUserManagedIdentityByResourceIdAuthConfig {
142 pub resource_id: *const c_char, }
144
145#[repr(C)]
146pub union GenevaAuthConfig {
147 pub cert: GenevaCertAuthConfig, pub workload_identity: GenevaWorkloadIdentityAuthConfig, pub user_msi: GenevaUserManagedIdentityAuthConfig, pub user_msi_objid: GenevaUserManagedIdentityByObjectIdAuthConfig, pub user_msi_resid: GenevaUserManagedIdentityByResourceIdAuthConfig, }
153
154#[repr(C)]
175pub struct GenevaConfig {
176 pub endpoint: *const c_char,
177 pub environment: *const c_char,
178 pub account: *const c_char,
179 pub namespace_name: *const c_char,
180 pub region: *const c_char,
181 pub config_major_version: c_uint,
182 pub auth_method: c_uint,
183 pub tenant: *const c_char,
184 pub role_name: *const c_char,
185 pub role_instance: *const c_char,
186 pub auth: GenevaAuthConfig, pub msi_resource: *const c_char, }
189
190#[repr(C)]
193#[derive(PartialEq)]
194pub enum GenevaError {
195 Success = 0,
197 InvalidConfig = 1,
198 InitializationFailed = 2,
199 UploadFailed = 3,
200 InvalidData = 4,
201 InternalError = 5,
202
203 NullPointer = 100,
205 EmptyInput = 101,
206 DecodeFailed = 102,
207 IndexOutOfRange = 103,
208 InvalidHandle = 104,
209
210 InvalidAuthMethod = 110,
212 InvalidCertConfig = 111,
213 InvalidWorkloadIdentityConfig = 112,
214 InvalidUserMsiConfig = 113,
215 InvalidUserMsiByObjectIdConfig = 114,
216 InvalidUserMsiByResourceIdConfig = 115,
217
218 MissingEndpoint = 130,
220 MissingEnvironment = 131,
221 MissingAccount = 132,
222 MissingNamespace = 133,
223 MissingRegion = 134,
224 MissingTenant = 135,
225 MissingRoleName = 136,
226 MissingRoleInstance = 137,
227}
228
229unsafe fn c_str_to_string(ptr: *const c_char, field_name: &str) -> Result<String, String> {
231 if ptr.is_null() {
232 return Err(format!("Field '{field_name}' is null"));
233 }
234
235 match unsafe { CStr::from_ptr(ptr) }.to_str() {
236 Ok(s) => Ok(s.to_string()),
237 Err(_) => Err(format!("Invalid UTF-8 in field '{field_name}'")),
238 }
239}
240
241unsafe fn write_error_if_provided(
246 err_msg_out: *mut c_char,
247 err_msg_len: usize,
248 error: &impl std::fmt::Display,
249) {
250 if !err_msg_out.is_null() && err_msg_len > 0 {
251 let error_string = error.to_string();
252 let bytes_to_copy = error_string.len().min(err_msg_len - 1);
253 if bytes_to_copy > 0 {
254 unsafe {
255 std::ptr::copy_nonoverlapping(
256 error_string.as_ptr() as *const c_char,
257 err_msg_out,
258 bytes_to_copy,
259 );
260 }
261 }
262 unsafe {
264 *err_msg_out.add(bytes_to_copy) = 0;
265 }
266 }
267}
268
269#[no_mangle]
281pub unsafe extern "C" fn geneva_client_new(
282 config: *const GenevaConfig,
283 out_handle: *mut *mut GenevaClientHandle,
284 err_msg_out: *mut c_char,
285 err_msg_len: usize,
286) -> GenevaError {
287 if config.is_null() || out_handle.is_null() {
289 return GenevaError::NullPointer;
290 }
291 unsafe { *out_handle = ptr::null_mut() };
292
293 let config = unsafe { config.as_ref().unwrap() };
294
295 if config.endpoint.is_null() {
297 return GenevaError::MissingEndpoint;
298 }
299 if config.environment.is_null() {
300 return GenevaError::MissingEnvironment;
301 }
302 if config.account.is_null() {
303 return GenevaError::MissingAccount;
304 }
305 if config.namespace_name.is_null() {
306 return GenevaError::MissingNamespace;
307 }
308 if config.region.is_null() {
309 return GenevaError::MissingRegion;
310 }
311 if config.tenant.is_null() {
312 return GenevaError::MissingTenant;
313 }
314 if config.role_name.is_null() {
315 return GenevaError::MissingRoleName;
316 }
317 if config.role_instance.is_null() {
318 return GenevaError::MissingRoleInstance;
319 }
320
321 let endpoint = match unsafe { c_str_to_string(config.endpoint, "endpoint") } {
323 Ok(s) => s,
324 Err(e) => {
325 unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
326 return GenevaError::InvalidConfig;
327 }
328 };
329 let environment = match unsafe { c_str_to_string(config.environment, "environment") } {
330 Ok(s) => s,
331 Err(e) => {
332 unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
333 return GenevaError::InvalidConfig;
334 }
335 };
336 let account = match unsafe { c_str_to_string(config.account, "account") } {
337 Ok(s) => s,
338 Err(e) => {
339 unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
340 return GenevaError::InvalidConfig;
341 }
342 };
343 let namespace = match unsafe { c_str_to_string(config.namespace_name, "namespace_name") } {
344 Ok(s) => s,
345 Err(e) => {
346 unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
347 return GenevaError::InvalidConfig;
348 }
349 };
350 let region = match unsafe { c_str_to_string(config.region, "region") } {
351 Ok(s) => s,
352 Err(e) => {
353 unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
354 return GenevaError::InvalidConfig;
355 }
356 };
357 let tenant = match unsafe { c_str_to_string(config.tenant, "tenant") } {
358 Ok(s) => s,
359 Err(e) => {
360 unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
361 return GenevaError::InvalidConfig;
362 }
363 };
364 let role_name = match unsafe { c_str_to_string(config.role_name, "role_name") } {
365 Ok(s) => s,
366 Err(e) => {
367 unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
368 return GenevaError::InvalidConfig;
369 }
370 };
371 let role_instance = match unsafe { c_str_to_string(config.role_instance, "role_instance") } {
372 Ok(s) => s,
373 Err(e) => {
374 unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
375 return GenevaError::InvalidConfig;
376 }
377 };
378
379 let auth_method = match config.auth_method {
381 0 => {
382 AuthMethod::SystemManagedIdentity
384 }
385
386 1 => {
387 let cert = unsafe { config.auth.cert };
389 if cert.cert_path.is_null() {
390 return GenevaError::InvalidCertConfig;
391 }
392 if cert.cert_password.is_null() {
393 return GenevaError::InvalidCertConfig;
394 }
395 let cert_path = match unsafe { c_str_to_string(cert.cert_path, "cert_path") } {
396 Ok(s) => PathBuf::from(s),
397 Err(e) => {
398 unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
399 return GenevaError::InvalidConfig;
400 }
401 };
402 let cert_password =
403 match unsafe { c_str_to_string(cert.cert_password, "cert_password") } {
404 Ok(s) => s,
405 Err(e) => {
406 unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
407 return GenevaError::InvalidConfig;
408 }
409 };
410 AuthMethod::Certificate {
411 path: cert_path,
412 password: cert_password,
413 }
414 }
415
416 2 => {
417 let workload_identity = unsafe { config.auth.workload_identity };
419 if workload_identity.resource.is_null() {
420 return GenevaError::InvalidWorkloadIdentityConfig;
421 }
422 let resource = match unsafe { c_str_to_string(workload_identity.resource, "resource") }
423 {
424 Ok(s) => s,
425 Err(e) => {
426 unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
427 return GenevaError::InvalidConfig;
428 }
429 };
430 AuthMethod::WorkloadIdentity { resource }
431 }
432
433 3 => {
434 let user_msi = unsafe { config.auth.user_msi };
436 if user_msi.client_id.is_null() {
437 return GenevaError::InvalidUserMsiConfig;
438 }
439 let client_id = match unsafe { c_str_to_string(user_msi.client_id, "client_id") } {
440 Ok(s) => s,
441 Err(e) => {
442 unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
443 return GenevaError::InvalidConfig;
444 }
445 };
446 AuthMethod::UserManagedIdentity { client_id }
447 }
448
449 4 => {
450 let user_msi_objid = unsafe { config.auth.user_msi_objid };
452 if user_msi_objid.object_id.is_null() {
453 return GenevaError::InvalidUserMsiByObjectIdConfig;
454 }
455 let object_id = match unsafe { c_str_to_string(user_msi_objid.object_id, "object_id") }
456 {
457 Ok(s) => s,
458 Err(e) => {
459 unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
460 return GenevaError::InvalidConfig;
461 }
462 };
463 AuthMethod::UserManagedIdentityByObjectId { object_id }
464 }
465
466 5 => {
467 let user_msi_resid = unsafe { config.auth.user_msi_resid };
469 if user_msi_resid.resource_id.is_null() {
470 return GenevaError::InvalidUserMsiByResourceIdConfig;
471 }
472 let resource_id =
473 match unsafe { c_str_to_string(user_msi_resid.resource_id, "resource_id") } {
474 Ok(s) => s,
475 Err(e) => {
476 unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
477 return GenevaError::InvalidConfig;
478 }
479 };
480 AuthMethod::UserManagedIdentityByResourceId { resource_id }
481 }
482
483 _ => {
484 return GenevaError::InvalidAuthMethod;
485 }
486 };
487
488 let msi_resource = if !config.msi_resource.is_null() {
490 match unsafe { c_str_to_string(config.msi_resource, "msi_resource") } {
491 Ok(s) => Some(s),
492 Err(e) => {
493 unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
494 return GenevaError::InvalidConfig;
495 }
496 }
497 } else {
498 None
499 };
500
501 let geneva_config = GenevaClientConfig {
503 endpoint,
504 environment,
505 account,
506 namespace,
507 region,
508 config_major_version: config.config_major_version,
509 auth_method,
510 tenant,
511 role_name,
512 role_instance,
513 msi_resource,
514 };
515
516 let client = match GenevaClient::new(geneva_config) {
518 Ok(client) => client,
519 Err(e) => {
520 unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
521 return GenevaError::InitializationFailed;
522 }
523 };
524
525 let handle = GenevaClientHandle {
526 magic: GENEVA_HANDLE_MAGIC,
527 client,
528 };
529 unsafe { *out_handle = Box::into_raw(Box::new(handle)) };
530 GenevaError::Success
531}
532
533#[no_mangle]
543pub unsafe extern "C" fn geneva_encode_and_compress_logs(
544 handle: *mut GenevaClientHandle,
545 data: *const u8,
546 data_len: usize,
547 out_batches: *mut *mut EncodedBatchesHandle,
548 err_msg_out: *mut c_char,
549 err_msg_len: usize,
550) -> GenevaError {
551 if out_batches.is_null() {
552 return GenevaError::NullPointer;
553 }
554 unsafe { *out_batches = ptr::null_mut() };
555
556 if handle.is_null() {
557 return GenevaError::NullPointer;
558 }
559 if data.is_null() {
560 return GenevaError::NullPointer;
561 }
562 if data_len == 0 {
563 return GenevaError::EmptyInput;
564 }
565
566 let validation_result = unsafe { validate_handle(handle) };
568 if validation_result != GenevaError::Success {
569 return validation_result;
570 }
571
572 let handle_ref = unsafe { handle.as_ref().unwrap() };
573 let data_slice = unsafe { std::slice::from_raw_parts(data, data_len) };
574
575 let logs_data: ExportLogsServiceRequest = match Message::decode(data_slice) {
576 Ok(data) => data,
577 Err(e) => {
578 unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
579 return GenevaError::DecodeFailed;
580 }
581 };
582
583 let resource_logs = logs_data.resource_logs;
584 match handle_ref.client.encode_and_compress_logs(&resource_logs) {
585 Ok(batches) => {
586 let h = EncodedBatchesHandle {
587 magic: GENEVA_HANDLE_MAGIC,
588 batches,
589 };
590 unsafe { *out_batches = Box::into_raw(Box::new(h)) };
591 GenevaError::Success
592 }
593 Err(e) => {
594 unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
595 GenevaError::InternalError
596 }
597 }
598}
599
600#[no_mangle]
610pub unsafe extern "C" fn geneva_encode_and_compress_spans(
611 handle: *mut GenevaClientHandle,
612 data: *const u8,
613 data_len: usize,
614 out_batches: *mut *mut EncodedBatchesHandle,
615 err_msg_out: *mut c_char,
616 err_msg_len: usize,
617) -> GenevaError {
618 if out_batches.is_null() {
619 return GenevaError::NullPointer;
620 }
621 unsafe { *out_batches = ptr::null_mut() };
622
623 if handle.is_null() {
624 return GenevaError::NullPointer;
625 }
626 if data.is_null() {
627 return GenevaError::NullPointer;
628 }
629 if data_len == 0 {
630 return GenevaError::EmptyInput;
631 }
632
633 let validation_result = unsafe { validate_handle(handle) };
635 if validation_result != GenevaError::Success {
636 return validation_result;
637 }
638
639 let handle_ref = unsafe { handle.as_ref().unwrap() };
640 let data_slice = unsafe { std::slice::from_raw_parts(data, data_len) };
641
642 let spans_data: ExportTraceServiceRequest = match Message::decode(data_slice) {
643 Ok(data) => data,
644 Err(e) => {
645 unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
646 return GenevaError::DecodeFailed;
647 }
648 };
649
650 let resource_spans = spans_data.resource_spans;
651 match handle_ref.client.encode_and_compress_spans(&resource_spans) {
652 Ok(batches) => {
653 let h = EncodedBatchesHandle {
654 magic: GENEVA_HANDLE_MAGIC,
655 batches,
656 };
657 unsafe { *out_batches = Box::into_raw(Box::new(h)) };
658 GenevaError::Success
659 }
660 Err(e) => {
661 unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
662 GenevaError::InternalError
663 }
664 }
665}
666
667#[no_mangle]
672pub unsafe extern "C" fn geneva_batches_len(batches: *const EncodedBatchesHandle) -> usize {
673 match unsafe { validate_handle(batches) } {
675 GenevaError::Success => {
676 let batches_ref = unsafe { batches.as_ref().unwrap() };
678 batches_ref.batches.len()
679 }
680 _ => 0, }
682}
683
684#[no_mangle]
693pub unsafe extern "C" fn geneva_upload_batch_sync(
694 handle: *mut GenevaClientHandle,
695 batches: *const EncodedBatchesHandle,
696 index: usize,
697 err_msg_out: *mut c_char,
698 err_msg_len: usize,
699) -> GenevaError {
700 match unsafe { validate_handle(handle) } {
702 GenevaError::Success => {}
703 error => return error,
704 }
705 match unsafe { validate_handle(batches) } {
707 GenevaError::Success => {}
708 error => return error,
709 }
710
711 let handle_ref = unsafe { handle.as_ref().unwrap() };
713 let batches_ref = unsafe { batches.as_ref().unwrap() };
714
715 if index >= batches_ref.batches.len() {
716 return GenevaError::IndexOutOfRange;
717 }
718
719 let batch = &batches_ref.batches[index];
720 let client = &handle_ref.client;
721 let res = runtime().block_on(async move { client.upload_batch(batch).await });
722 match res {
723 Ok(_) => GenevaError::Success,
724 Err(e) => {
725 unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
726 GenevaError::UploadFailed
727 }
728 }
729}
730
731#[no_mangle]
737pub unsafe extern "C" fn geneva_batches_free(batches: *mut EncodedBatchesHandle) {
738 if !batches.is_null() {
739 unsafe { clear_handle_magic(batches) };
740 let _ = unsafe { Box::from_raw(batches) };
741 }
742}
743
744#[no_mangle]
750pub unsafe extern "C" fn geneva_client_free(handle: *mut GenevaClientHandle) {
751 if !handle.is_null() {
752 unsafe { clear_handle_magic(handle) };
753 let _ = unsafe { Box::from_raw(handle) };
754 }
755}
756
757#[cfg(test)]
758mod tests {
759 use super::*;
760 use std::ffi::CString;
761
762 #[allow(dead_code)]
764 fn generate_mock_jwt_and_expiry(endpoint: &str, ttl_secs: i64) -> (String, String) {
765 use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
766 use chrono::{Duration, Utc};
767
768 let header = r#"{"alg":"none","typ":"JWT"}"#;
769 let exp = Utc::now() + Duration::seconds(ttl_secs);
770 let payload = format!(r#"{{"Endpoint":"{endpoint}","exp":{}}}"#, exp.timestamp());
771
772 let header_b64 = URL_SAFE_NO_PAD.encode(header.as_bytes());
773 let payload_b64 = URL_SAFE_NO_PAD.encode(payload.as_bytes());
774 let token = format!("{}.{}.{sig}", header_b64, payload_b64, sig = "dummy");
775
776 (token, exp.to_rfc3339())
777 }
778
779 #[test]
780 fn test_geneva_client_new_with_null_config() {
781 unsafe {
782 let mut out: *mut GenevaClientHandle = std::ptr::null_mut();
783 let rc = geneva_client_new(std::ptr::null(), &mut out, ptr::null_mut(), 0);
784 assert_eq!(rc as u32, GenevaError::NullPointer as u32);
785 assert!(out.is_null());
786 }
787 }
788
789 #[test]
790 fn test_upload_batch_sync_with_nulls() {
791 unsafe {
792 let result =
793 geneva_upload_batch_sync(ptr::null_mut(), ptr::null(), 0, ptr::null_mut(), 0);
794 assert_eq!(result as u32, GenevaError::NullPointer as u32);
795 }
796 }
797
798 #[test]
799 fn test_encode_with_nulls() {
800 unsafe {
801 let mut out: *mut EncodedBatchesHandle = std::ptr::null_mut();
802 let rc = geneva_encode_and_compress_logs(
803 ptr::null_mut(),
804 ptr::null(),
805 0,
806 &mut out,
807 ptr::null_mut(),
808 0,
809 );
810 assert_eq!(rc as u32, GenevaError::NullPointer as u32);
811 assert!(out.is_null());
812 }
813 }
814
815 #[test]
816 fn test_geneva_client_free_with_null() {
817 unsafe {
818 geneva_client_free(ptr::null_mut());
820 }
821 }
822
823 #[test]
824 fn test_null_field_validation() {
825 unsafe {
826 let environment = CString::new("test").unwrap();
828 let account = CString::new("testaccount").unwrap();
829 let namespace = CString::new("testns").unwrap();
830 let region = CString::new("testregion").unwrap();
831 let tenant = CString::new("testtenant").unwrap();
832 let role_name = CString::new("testrole").unwrap();
833 let role_instance = CString::new("testinstance").unwrap();
834
835 let config = GenevaConfig {
836 endpoint: ptr::null(), environment: environment.as_ptr(),
838 account: account.as_ptr(),
839 namespace_name: namespace.as_ptr(),
840 region: region.as_ptr(),
841 config_major_version: 1,
842 auth_method: 0, tenant: tenant.as_ptr(),
844 role_name: role_name.as_ptr(),
845 role_instance: role_instance.as_ptr(),
846 auth: std::mem::zeroed(),
850 msi_resource: ptr::null(),
851 };
852
853 let mut out: *mut GenevaClientHandle = std::ptr::null_mut();
854 let rc = geneva_client_new(&config, &mut out, ptr::null_mut(), 0);
855 assert_eq!(rc as u32, GenevaError::MissingEndpoint as u32);
856 assert!(out.is_null());
857 }
858 }
859
860 #[test]
861 fn test_invalid_auth_method() {
862 unsafe {
863 let endpoint = CString::new("https://test.geneva.com").unwrap();
864 let environment = CString::new("test").unwrap();
865 let account = CString::new("testaccount").unwrap();
866 let namespace = CString::new("testns").unwrap();
867 let region = CString::new("testregion").unwrap();
868 let tenant = CString::new("testtenant").unwrap();
869 let role_name = CString::new("testrole").unwrap();
870 let role_instance = CString::new("testinstance").unwrap();
871
872 let config = GenevaConfig {
873 endpoint: endpoint.as_ptr(),
874 environment: environment.as_ptr(),
875 account: account.as_ptr(),
876 namespace_name: namespace.as_ptr(),
877 region: region.as_ptr(),
878 config_major_version: 1,
879 auth_method: 99, tenant: tenant.as_ptr(),
881 role_name: role_name.as_ptr(),
882 role_instance: role_instance.as_ptr(),
883 auth: std::mem::zeroed(), msi_resource: ptr::null(),
885 };
886
887 let mut out: *mut GenevaClientHandle = std::ptr::null_mut();
888 let rc = geneva_client_new(&config, &mut out, ptr::null_mut(), 0);
889 assert_eq!(rc as u32, GenevaError::InvalidAuthMethod as u32);
890 assert!(out.is_null());
891 }
892 }
893
894 #[test]
895 fn test_certificate_auth_missing_cert_path() {
896 unsafe {
897 let endpoint = CString::new("https://test.geneva.com").unwrap();
898 let environment = CString::new("test").unwrap();
899 let account = CString::new("testaccount").unwrap();
900 let namespace = CString::new("testns").unwrap();
901 let region = CString::new("testregion").unwrap();
902 let tenant = CString::new("testtenant").unwrap();
903 let role_name = CString::new("testrole").unwrap();
904 let role_instance = CString::new("testinstance").unwrap();
905
906 let config = GenevaConfig {
907 endpoint: endpoint.as_ptr(),
908 environment: environment.as_ptr(),
909 account: account.as_ptr(),
910 namespace_name: namespace.as_ptr(),
911 region: region.as_ptr(),
912 config_major_version: 1,
913 auth_method: 1, tenant: tenant.as_ptr(),
915 role_name: role_name.as_ptr(),
916 role_instance: role_instance.as_ptr(),
917 auth: GenevaAuthConfig {
918 cert: GenevaCertAuthConfig {
919 cert_path: ptr::null(),
920 cert_password: ptr::null(),
921 },
922 },
923 msi_resource: ptr::null(),
924 };
925
926 let mut out: *mut GenevaClientHandle = std::ptr::null_mut();
927 let rc = geneva_client_new(&config, &mut out, ptr::null_mut(), 0);
928 assert_eq!(rc as u32, GenevaError::InvalidCertConfig as u32);
929 assert!(out.is_null());
930 }
931 }
932
933 #[test]
934 fn test_workload_identity_auth_missing_resource() {
935 unsafe {
936 let endpoint = CString::new("https://test.geneva.com").unwrap();
937 let environment = CString::new("test").unwrap();
938 let account = CString::new("testaccount").unwrap();
939 let namespace = CString::new("testns").unwrap();
940 let region = CString::new("testregion").unwrap();
941 let tenant = CString::new("testtenant").unwrap();
942 let role_name = CString::new("testrole").unwrap();
943 let role_instance = CString::new("testinstance").unwrap();
944
945 let config = GenevaConfig {
946 endpoint: endpoint.as_ptr(),
947 environment: environment.as_ptr(),
948 account: account.as_ptr(),
949 namespace_name: namespace.as_ptr(),
950 region: region.as_ptr(),
951 config_major_version: 1,
952 auth_method: 2, tenant: tenant.as_ptr(),
954 role_name: role_name.as_ptr(),
955 role_instance: role_instance.as_ptr(),
956 auth: GenevaAuthConfig {
957 workload_identity: GenevaWorkloadIdentityAuthConfig {
958 resource: ptr::null(),
959 },
960 },
961 msi_resource: ptr::null(),
962 };
963
964 let mut out: *mut GenevaClientHandle = std::ptr::null_mut();
965 let rc = geneva_client_new(&config, &mut out, ptr::null_mut(), 0);
966 assert_eq!(rc as u32, GenevaError::InvalidWorkloadIdentityConfig as u32);
967 assert!(out.is_null());
968 }
969 }
970
971 #[test]
972 fn test_user_msi_auth_missing_client_id() {
973 unsafe {
974 let endpoint = CString::new("https://test.geneva.com").unwrap();
975 let environment = CString::new("test").unwrap();
976 let account = CString::new("testaccount").unwrap();
977 let namespace = CString::new("testns").unwrap();
978 let region = CString::new("testregion").unwrap();
979 let tenant = CString::new("testtenant").unwrap();
980 let role_name = CString::new("testrole").unwrap();
981 let role_instance = CString::new("testinstance").unwrap();
982
983 let config = GenevaConfig {
984 endpoint: endpoint.as_ptr(),
985 environment: environment.as_ptr(),
986 account: account.as_ptr(),
987 namespace_name: namespace.as_ptr(),
988 region: region.as_ptr(),
989 config_major_version: 1,
990 auth_method: 3, tenant: tenant.as_ptr(),
992 role_name: role_name.as_ptr(),
993 role_instance: role_instance.as_ptr(),
994 auth: GenevaAuthConfig {
995 user_msi: GenevaUserManagedIdentityAuthConfig {
996 client_id: ptr::null(),
997 },
998 },
999 msi_resource: ptr::null(),
1000 };
1001
1002 let mut out: *mut GenevaClientHandle = std::ptr::null_mut();
1003 let rc = geneva_client_new(&config, &mut out, ptr::null_mut(), 0);
1004 assert_eq!(rc as u32, GenevaError::InvalidUserMsiConfig as u32);
1005 assert!(out.is_null());
1006 }
1007 }
1008
1009 #[test]
1010 fn test_user_msi_auth_by_object_id_missing() {
1011 unsafe {
1012 let endpoint = CString::new("https://test.geneva.com").unwrap();
1013 let environment = CString::new("test").unwrap();
1014 let account = CString::new("testaccount").unwrap();
1015 let namespace = CString::new("testns").unwrap();
1016 let region = CString::new("testregion").unwrap();
1017 let tenant = CString::new("testtenant").unwrap();
1018 let role_name = CString::new("testrole").unwrap();
1019 let role_instance = CString::new("testinstance").unwrap();
1020
1021 let config = GenevaConfig {
1022 endpoint: endpoint.as_ptr(),
1023 environment: environment.as_ptr(),
1024 account: account.as_ptr(),
1025 namespace_name: namespace.as_ptr(),
1026 region: region.as_ptr(),
1027 config_major_version: 1,
1028 auth_method: 4, tenant: tenant.as_ptr(),
1030 role_name: role_name.as_ptr(),
1031 role_instance: role_instance.as_ptr(),
1032 auth: GenevaAuthConfig {
1033 user_msi_objid: GenevaUserManagedIdentityByObjectIdAuthConfig {
1034 object_id: ptr::null(),
1035 },
1036 },
1037 msi_resource: ptr::null(),
1038 };
1039
1040 let mut out: *mut GenevaClientHandle = std::ptr::null_mut();
1041 let rc = geneva_client_new(&config, &mut out, ptr::null_mut(), 0);
1042 assert_eq!(
1043 rc as u32,
1044 GenevaError::InvalidUserMsiByObjectIdConfig as u32
1045 );
1046 assert!(out.is_null());
1047 }
1048 }
1049
1050 #[test]
1051 fn test_user_msi_auth_by_resource_id_missing() {
1052 unsafe {
1053 let endpoint = CString::new("https://test.geneva.com").unwrap();
1054 let environment = CString::new("test").unwrap();
1055 let account = CString::new("testaccount").unwrap();
1056 let namespace = CString::new("testns").unwrap();
1057 let region = CString::new("testregion").unwrap();
1058 let tenant = CString::new("testtenant").unwrap();
1059 let role_name = CString::new("testrole").unwrap();
1060 let role_instance = CString::new("testinstance").unwrap();
1061
1062 let config = GenevaConfig {
1063 endpoint: endpoint.as_ptr(),
1064 environment: environment.as_ptr(),
1065 account: account.as_ptr(),
1066 namespace_name: namespace.as_ptr(),
1067 region: region.as_ptr(),
1068 config_major_version: 1,
1069 auth_method: 5, tenant: tenant.as_ptr(),
1071 role_name: role_name.as_ptr(),
1072 role_instance: role_instance.as_ptr(),
1073 auth: GenevaAuthConfig {
1074 user_msi_resid: GenevaUserManagedIdentityByResourceIdAuthConfig {
1075 resource_id: ptr::null(),
1076 },
1077 },
1078 msi_resource: ptr::null(),
1079 };
1080
1081 let mut out: *mut GenevaClientHandle = std::ptr::null_mut();
1082 let rc = geneva_client_new(&config, &mut out, ptr::null_mut(), 0);
1083 assert_eq!(
1084 rc as u32,
1085 GenevaError::InvalidUserMsiByResourceIdConfig as u32
1086 );
1087 assert!(out.is_null());
1088 }
1089 }
1090
1091 #[test]
1092 fn test_certificate_auth_missing_cert_password() {
1093 unsafe {
1094 let endpoint = CString::new("https://test.geneva.com").unwrap();
1095 let environment = CString::new("test").unwrap();
1096 let account = CString::new("testaccount").unwrap();
1097 let namespace = CString::new("testns").unwrap();
1098 let region = CString::new("testregion").unwrap();
1099 let tenant = CString::new("testtenant").unwrap();
1100 let role_name = CString::new("testrole").unwrap();
1101 let role_instance = CString::new("testinstance").unwrap();
1102 let cert_path = CString::new("/path/to/cert.p12").unwrap();
1103
1104 let config = GenevaConfig {
1105 endpoint: endpoint.as_ptr(),
1106 environment: environment.as_ptr(),
1107 account: account.as_ptr(),
1108 namespace_name: namespace.as_ptr(),
1109 region: region.as_ptr(),
1110 config_major_version: 1,
1111 auth_method: 1, tenant: tenant.as_ptr(),
1113 role_name: role_name.as_ptr(),
1114 role_instance: role_instance.as_ptr(),
1115 auth: GenevaAuthConfig {
1116 cert: GenevaCertAuthConfig {
1117 cert_path: cert_path.as_ptr(),
1118 cert_password: ptr::null(),
1119 },
1120 },
1121 msi_resource: ptr::null(),
1122 };
1123
1124 let mut out: *mut GenevaClientHandle = std::ptr::null_mut();
1125 let rc = geneva_client_new(&config, &mut out, ptr::null_mut(), 0);
1126 assert_eq!(rc as u32, GenevaError::InvalidCertConfig as u32);
1127 assert!(out.is_null());
1128 }
1129 }
1130
1131 #[test]
1132 fn test_batches_len_with_null() {
1133 unsafe {
1134 let n = geneva_batches_len(ptr::null());
1135 assert_eq!(n, 0, "batches_len should return 0 for null pointer");
1136 }
1137 }
1138
1139 #[test]
1140 fn test_batches_free_with_null() {
1141 unsafe {
1142 geneva_batches_free(ptr::null_mut());
1143 }
1144 }
1145
1146 #[test]
1149 #[cfg(feature = "mock_auth")]
1150 fn test_encode_and_upload_with_mock_server() {
1151 use otlp_builder::builder::build_otlp_logs_minimal;
1152 use wiremock::matchers::method;
1153 use wiremock::{Mock, MockServer, ResponseTemplate};
1154
1155 let mock_server = runtime().block_on(async { MockServer::start().await });
1157 let ingestion_endpoint = mock_server.uri();
1158
1159 let (auth_token, auth_token_expiry) =
1161 generate_mock_jwt_and_expiry(&ingestion_endpoint, 24 * 3600);
1162
1163 runtime().block_on(async {
1165 Mock::given(method("GET"))
1166 .respond_with(ResponseTemplate::new(200).set_body_string(format!(
1167 r#"{{
1168 "IngestionGatewayInfo": {{
1169 "Endpoint": "{ingestion_endpoint}",
1170 "AuthToken": "{auth_token}",
1171 "AuthTokenExpiryTime": "{auth_token_expiry}"
1172 }},
1173 "StorageAccountKeys": [{{
1174 "AccountMonikerName": "testdiagaccount",
1175 "AccountGroupName": "testgroup",
1176 "IsPrimaryMoniker": true
1177 }}],
1178 "TagId": "test"
1179 }}"#
1180 )))
1181 .mount(&mock_server)
1182 .await;
1183
1184 Mock::given(method("POST"))
1186 .respond_with(
1187 ResponseTemplate::new(202).set_body_string(r#"{"ticket":"accepted"}"#),
1188 )
1189 .mount(&mock_server)
1190 .await;
1191 });
1192
1193 let cfg = GenevaClientConfig {
1195 endpoint: mock_server.uri(),
1196 environment: "test".to_string(),
1197 account: "test".to_string(),
1198 namespace: "testns".to_string(),
1199 region: "testregion".to_string(),
1200 config_major_version: 1,
1201 auth_method: AuthMethod::MockAuth,
1202 tenant: "testtenant".to_string(),
1203 role_name: "testrole".to_string(),
1204 role_instance: "testinstance".to_string(),
1205 msi_resource: None,
1206 };
1207 let client = GenevaClient::new(cfg).expect("failed to create GenevaClient with MockAuth");
1208
1209 let handle = GenevaClientHandle {
1211 magic: GENEVA_HANDLE_MAGIC,
1212 client,
1213 };
1214 let mut handle_box = Box::new(handle);
1216 let handle_ptr: *mut GenevaClientHandle = &mut *handle_box;
1217
1218 let bytes = build_otlp_logs_minimal("TestEvent", "hello-world", Some(("rk", "rv")));
1220
1221 let mut batches_ptr: *mut EncodedBatchesHandle = std::ptr::null_mut();
1223 let rc = unsafe {
1224 geneva_encode_and_compress_logs(
1225 handle_ptr,
1226 bytes.as_ptr(),
1227 bytes.len(),
1228 &mut batches_ptr,
1229 ptr::null_mut(),
1230 0,
1231 )
1232 };
1233 assert_eq!(rc as u32, GenevaError::Success as u32, "encode failed");
1234 assert!(
1235 !batches_ptr.is_null(),
1236 "out_batches should be non-null on success"
1237 );
1238
1239 let len = unsafe { geneva_batches_len(batches_ptr) };
1241 assert!(len >= 1, "expected at least one encoded batch");
1242
1243 let _ = unsafe {
1245 geneva_upload_batch_sync(handle_ptr, batches_ptr as *const _, 0, ptr::null_mut(), 0)
1246 };
1247
1248 unsafe {
1250 geneva_batches_free(batches_ptr);
1251 }
1252 let raw_handle = Box::into_raw(handle_box);
1254 unsafe {
1255 geneva_client_free(raw_handle);
1256 }
1257
1258 drop(mock_server);
1260 }
1261
1262 #[test]
1266 #[cfg(feature = "mock_auth")]
1267 fn test_encode_batching_by_event_name_and_upload() {
1268 use wiremock::http::Method;
1269 use wiremock::matchers::method;
1270 use wiremock::{Mock, MockServer, ResponseTemplate};
1271
1272 let mock_server = runtime().block_on(async { MockServer::start().await });
1274 let ingestion_endpoint = mock_server.uri();
1275 let (auth_token, auth_token_expiry) =
1276 generate_mock_jwt_and_expiry(&ingestion_endpoint, 24 * 3600);
1277
1278 runtime().block_on(async {
1280 Mock::given(method("GET"))
1281 .respond_with(ResponseTemplate::new(200).set_body_string(format!(
1282 r#"{{
1283 "IngestionGatewayInfo": {{
1284 "Endpoint": "{ingestion_endpoint}",
1285 "AuthToken": "{auth_token}",
1286 "AuthTokenExpiryTime": "{auth_token_expiry}"
1287 }},
1288 "StorageAccountKeys": [{{
1289 "AccountMonikerName": "testdiagaccount",
1290 "AccountGroupName": "testgroup",
1291 "IsPrimaryMoniker": true
1292 }}],
1293 "TagId": "test"
1294 }}"#
1295 )))
1296 .mount(&mock_server)
1297 .await;
1298
1299 Mock::given(method("POST"))
1300 .respond_with(
1301 ResponseTemplate::new(202).set_body_string(r#"{"ticket":"accepted"}"#),
1302 )
1303 .mount(&mock_server)
1304 .await;
1305 });
1306
1307 let cfg = GenevaClientConfig {
1309 endpoint: mock_server.uri(),
1310 environment: "test".to_string(),
1311 account: "test".to_string(),
1312 namespace: "testns".to_string(),
1313 region: "testregion".to_string(),
1314 config_major_version: 1,
1315 auth_method: AuthMethod::MockAuth,
1316 tenant: "testtenant".to_string(),
1317 role_name: "testrole".to_string(),
1318 role_instance: "testinstance".to_string(),
1319 msi_resource: None,
1320 };
1321 let client = GenevaClient::new(cfg).expect("failed to create GenevaClient with MockAuth");
1322
1323 let mut handle_box = Box::new(GenevaClientHandle {
1325 magic: GENEVA_HANDLE_MAGIC,
1326 client,
1327 });
1328 let handle_ptr: *mut GenevaClientHandle = &mut *handle_box;
1329
1330 let log1 = opentelemetry_proto::tonic::logs::v1::LogRecord {
1332 observed_time_unix_nano: 1_700_000_000_000_000_001,
1333 event_name: "EventA".to_string(),
1334 severity_number: 9,
1335 ..Default::default()
1336 };
1337 let log2 = opentelemetry_proto::tonic::logs::v1::LogRecord {
1338 observed_time_unix_nano: 1_700_000_000_000_000_002,
1339 event_name: "EventB".to_string(),
1340 severity_number: 10,
1341 ..Default::default()
1342 };
1343 let scope_logs = opentelemetry_proto::tonic::logs::v1::ScopeLogs {
1344 log_records: vec![log1, log2],
1345 ..Default::default()
1346 };
1347 let resource_logs = opentelemetry_proto::tonic::logs::v1::ResourceLogs {
1348 scope_logs: vec![scope_logs],
1349 ..Default::default()
1350 };
1351 let req = ExportLogsServiceRequest {
1352 resource_logs: vec![resource_logs],
1353 };
1354 let bytes = req.encode_to_vec();
1355
1356 let mut batches_ptr: *mut EncodedBatchesHandle = std::ptr::null_mut();
1358 let rc = unsafe {
1359 geneva_encode_and_compress_logs(
1360 handle_ptr,
1361 bytes.as_ptr(),
1362 bytes.len(),
1363 &mut batches_ptr,
1364 ptr::null_mut(),
1365 0,
1366 )
1367 };
1368 assert_eq!(rc as u32, GenevaError::Success as u32, "encode failed");
1369 assert!(!batches_ptr.is_null());
1370
1371 let len = unsafe { geneva_batches_len(batches_ptr) };
1373 assert_eq!(len, 2, "expected 2 batches grouped by event_name");
1374
1375 for i in 0..len {
1377 let _ = unsafe {
1378 geneva_upload_batch_sync(handle_ptr, batches_ptr as *const _, i, ptr::null_mut(), 0)
1379 };
1380 }
1381
1382 let (urls, has_a, has_b) = runtime().block_on(async {
1385 use tokio::time::{sleep, Duration};
1386 let mut last_urls: Vec<String> = Vec::new();
1387 for _ in 0..200 {
1388 let reqs = mock_server.received_requests().await.unwrap();
1389 let posts: Vec<String> = reqs
1390 .iter()
1391 .filter(|r| r.method == Method::Post)
1392 .map(|r| r.url.to_string())
1393 .collect();
1394
1395 let has_a = posts.iter().any(|u| u.contains("event=EventA"));
1396 let has_b = posts.iter().any(|u| u.contains("event=EventB"));
1397 if has_a && has_b {
1398 return (posts, true, true);
1399 }
1400
1401 if !posts.is_empty() {
1402 last_urls = posts.clone();
1403 }
1404
1405 sleep(Duration::from_millis(20)).await;
1406 }
1407
1408 if last_urls.is_empty() {
1409 let reqs = mock_server.received_requests().await.unwrap();
1410 last_urls = reqs.into_iter().map(|r| r.url.to_string()).collect();
1411 }
1412 let has_a = last_urls.iter().any(|u| u.contains("event=EventA"));
1413 let has_b = last_urls.iter().any(|u| u.contains("event=EventB"));
1414 (last_urls, has_a, has_b)
1415 });
1416 assert!(
1417 has_a,
1418 "Expected request containing event=EventA; got: {urls:?}"
1419 );
1420 assert!(
1421 has_b,
1422 "Expected request containing event=EventB; got: {urls:?}"
1423 );
1424
1425 unsafe { geneva_batches_free(batches_ptr) };
1427 let raw_handle = Box::into_raw(handle_box);
1428 unsafe { geneva_client_free(raw_handle) };
1429 drop(mock_server);
1430 }
1431}