Skip to content

Commit 471aa06

Browse files
authored
feat: [Geneva Exporter] FFI Interface for Geneva Exporter (#419)
1 parent d7784b1 commit 471aa06

File tree

14 files changed

+1583
-15
lines changed

14 files changed

+1583
-15
lines changed

opentelemetry-exporter-geneva/geneva-uploader-ffi/Cargo.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,23 @@ edition = "2021"
55
license = "Apache-2.0"
66
rust-version = "1.75.0"
77

8+
[lib]
9+
crate-type = ["cdylib", "staticlib", "rlib"]
10+
811
[dependencies]
12+
geneva-uploader = { path = "../geneva-uploader" }
13+
opentelemetry-proto = { workspace = true, default-features = false, features = ["logs", "gen-tonic-messages"] }
14+
tokio = { version = "1.0", features = ["rt-multi-thread"] }
15+
prost = "0.13"
16+
17+
[features]
18+
mock_auth = ["geneva-uploader/mock_auth"]
919

1020
[lints]
1121
workspace = true
22+
23+
[dev-dependencies]
24+
otlp_builder = { path = "examples/otlp_builder" }
25+
wiremock = "0.6"
26+
base64 = "0.22"
27+
chrono = "0.4"
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Makefile for building and running the C FFI example (local to examples/)
2+
# This focuses only on compiling c_example.c against the built Rust FFI libs.
3+
4+
.PHONY: all run build-rust build-otlp c-example clean verify-header help
5+
6+
# Defaults: build only (do not run)
7+
all: build-rust build-otlp $(BINARY)
8+
9+
# Paths relative to this examples/ directory
10+
RUST_CRATE_DIR := ..
11+
INCLUDE_DIR := ../include
12+
LIB_DIR := ../../../target/release
13+
LIB_DIR_DEPS := $(LIB_DIR)/deps
14+
BINARY := c_example_test
15+
16+
# Build the Rust FFI library (release)
17+
build-rust:
18+
@echo "Building Rust FFI library..."
19+
@cd $(RUST_CRATE_DIR) && cargo build --release
20+
21+
# Build the example-only otlp_builder (release)
22+
build-otlp:
23+
@echo "Building otlp_builder (cdylib + rlib)..."
24+
@cd otlp_builder && cargo build --release
25+
26+
# Build the C example binary
27+
$(BINARY): c_example.c build-rust build-otlp
28+
@echo "Building C example..."
29+
@gcc -std=c11 -o $(BINARY) c_example.c \
30+
-I$(INCLUDE_DIR) \
31+
-L$(LIB_DIR) -L$(LIB_DIR_DEPS) \
32+
-lgeneva_uploader_ffi -lotlp_builder \
33+
-Wl,-rpath,@loader_path/../../../target/release \
34+
-Wl,-rpath,@loader_path/../../../target/release/deps \
35+
-lpthread -ldl -lm
36+
37+
# Run the example with proper dynamic library path for macOS/Linux
38+
run: $(BINARY)
39+
@echo "Running C example..."
40+
@DYLD_LIBRARY_PATH=$(LIB_DIR):$(LIB_DIR_DEPS) LD_LIBRARY_PATH=$(LIB_DIR):$(LIB_DIR_DEPS) ./$(BINARY)
41+
42+
# Alias
43+
c-example: run
44+
45+
# Quick header verification (compile-only)
46+
verify-header:
47+
@echo "Verifying C header compatibility..."
48+
@gcc -c c_example.c -I$(INCLUDE_DIR) -o /tmp/test_header.o && echo "✓ C header is valid" || echo "✗ C header has issues"
49+
@rm -f /tmp/test_header.o
50+
51+
# Clean example build artifacts
52+
clean:
53+
@rm -f $(BINARY)
54+
55+
help:
56+
@echo "Targets:"
57+
@echo " all - Build Rust lib + otlp_builder + C example binary (default)"
58+
@echo " run - Build and run the C example (requires env vars)"
59+
@echo " c-example - Same as 'run'"
60+
@echo " build-rust - Build the Rust FFI library in release mode"
61+
@echo " build-otlp - Build the otlp_builder cdylib/rlib (release)"
62+
@echo " verify-header - Compile-check the header with the example"
63+
@echo " clean - Remove the C example binary"
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*
2+
* Geneva FFI C Example (synchronous only)
3+
*
4+
* This example demonstrates:
5+
* - Reading configuration from environment
6+
* - Creating a Geneva client via geneva_client_new (out-param)
7+
* - Encoding/compressing ResourceLogs
8+
* - Uploading batches synchronously with geneva_upload_batch_sync
9+
*
10+
* Note: The non-blocking callback-based mechanism has been removed.
11+
*/
12+
13+
#include <stdio.h>
14+
#include <stdlib.h>
15+
#include <string.h>
16+
#include <time.h>
17+
#include <stdint.h>
18+
#include "../include/geneva_ffi.h"
19+
20+
/* Prototypes from the example-only builder dylib (otlp_builder) */
21+
extern int geneva_build_otlp_logs_minimal(const char* body_utf8,
22+
const char* resource_key,
23+
const char* resource_value,
24+
uint8_t** out_ptr,
25+
size_t* out_len);
26+
extern void geneva_free_buffer(uint8_t* ptr, size_t len);
27+
28+
/* Helper to read env or default */
29+
static const char* get_env_or_default(const char* name, const char* defval) {
30+
const char* v = getenv(name);
31+
return v ? v : defval;
32+
}
33+
34+
35+
int main(void) {
36+
printf("Geneva FFI Example (synchronous API)\n");
37+
printf("====================================\n\n");
38+
39+
/* Required env */
40+
const char* endpoint = getenv("GENEVA_ENDPOINT");
41+
const char* environment = getenv("GENEVA_ENVIRONMENT");
42+
const char* account = getenv("GENEVA_ACCOUNT");
43+
const char* namespaceName = getenv("GENEVA_NAMESPACE");
44+
const char* region = getenv("GENEVA_REGION");
45+
const char* cfg_ver_str = getenv("GENEVA_CONFIG_MAJOR_VERSION");
46+
47+
if (!endpoint || !environment || !account || !namespaceName || !region || !cfg_ver_str) {
48+
printf("Missing required environment variables!\n");
49+
printf(" GENEVA_ENDPOINT\n");
50+
printf(" GENEVA_ENVIRONMENT\n");
51+
printf(" GENEVA_ACCOUNT\n");
52+
printf(" GENEVA_NAMESPACE\n");
53+
printf(" GENEVA_REGION\n");
54+
printf(" GENEVA_CONFIG_MAJOR_VERSION\n");
55+
return 1;
56+
}
57+
58+
int cfg_ver = atoi(cfg_ver_str);
59+
if (cfg_ver <= 0) {
60+
printf("Invalid GENEVA_CONFIG_MAJOR_VERSION: %s\n", cfg_ver_str);
61+
return 1;
62+
}
63+
64+
/* Optional env with defaults */
65+
const char* tenant = get_env_or_default("GENEVA_TENANT", "default-tenant");
66+
const char* role_name = get_env_or_default("GENEVA_ROLE_NAME", "default-role");
67+
const char* role_instance= get_env_or_default("GENEVA_ROLE_INSTANCE", "default-instance");
68+
69+
/* Certificate auth if both provided; otherwise managed identity */
70+
const char* cert_path = getenv("GENEVA_CERT_PATH");
71+
const char* cert_password = getenv("GENEVA_CERT_PASSWORD");
72+
int32_t auth_method = (cert_path && cert_password) ? GENEVA_AUTH_CERTIFICATE : GENEVA_AUTH_MANAGED_IDENTITY;
73+
74+
printf("Configuration:\n");
75+
printf(" Endpoint: %s\n", endpoint);
76+
printf(" Environment: %s\n", environment);
77+
printf(" Account: %s\n", account);
78+
printf(" Namespace: %s\n", namespaceName);
79+
printf(" Region: %s\n", region);
80+
printf(" Config Major Version: %d\n", cfg_ver);
81+
printf(" Tenant: %s\n", tenant);
82+
printf(" Role Name: %s\n", role_name);
83+
printf(" Role Instance: %s\n", role_instance);
84+
printf(" Auth Method: %s\n", auth_method == GENEVA_AUTH_CERTIFICATE ? "Certificate" : "Managed Identity");
85+
if (auth_method == GENEVA_AUTH_CERTIFICATE) {
86+
printf(" Cert Path: %s\n", cert_path);
87+
}
88+
printf("\n");
89+
90+
/* Build config */
91+
GenevaConfig cfg = {
92+
.endpoint = endpoint,
93+
.environment = environment,
94+
.account = account,
95+
.namespace_name = namespaceName,
96+
.region = region,
97+
.config_major_version = (uint32_t)cfg_ver,
98+
.auth_method = auth_method,
99+
.tenant = tenant,
100+
.role_name = role_name,
101+
.role_instance = role_instance,
102+
};
103+
if (auth_method == GENEVA_AUTH_CERTIFICATE) {
104+
cfg.auth.cert.cert_path = cert_path;
105+
cfg.auth.cert.cert_password = cert_password;
106+
} else {
107+
cfg.auth.msi.objid = NULL;
108+
}
109+
110+
/* Create client */
111+
GenevaClientHandle* client = NULL;
112+
GenevaError rc = geneva_client_new(&cfg, &client);
113+
if (rc != GENEVA_SUCCESS || client == NULL) {
114+
printf("Failed to create Geneva client (code=%d)\n", rc);
115+
return 1;
116+
}
117+
printf("Geneva client created.\n");
118+
119+
/* Create ExportLogsServiceRequest bytes via FFI builder */
120+
size_t data_len = 0;
121+
uint8_t* data = NULL;
122+
GenevaError brc = geneva_build_otlp_logs_minimal("hello from c ffi", "service.name", "c-ffi-example", &data, &data_len);
123+
if (brc != GENEVA_SUCCESS || data == NULL || data_len == 0) {
124+
printf("Failed to build OTLP payload (code=%d)\n", brc);
125+
geneva_client_free(client);
126+
return 1;
127+
}
128+
129+
/* Encode and compress to batches */
130+
EncodedBatchesHandle* batches = NULL;
131+
GenevaError enc_rc = geneva_encode_and_compress_logs(client, data, data_len, &batches);
132+
if (enc_rc != GENEVA_SUCCESS || batches == NULL) {
133+
printf("Encode/compress failed (code=%d)\n", enc_rc);
134+
geneva_free_buffer(data, data_len);
135+
geneva_client_free(client);
136+
return 1;
137+
}
138+
139+
size_t n = geneva_batches_len(batches);
140+
printf("Encoded %zu batch(es)\n", n);
141+
142+
/* Upload synchronously, batch by batch */
143+
GenevaError first_err = GENEVA_SUCCESS;
144+
for (size_t i = 0; i < n; i++) {
145+
GenevaError r = geneva_upload_batch_sync(client, batches, i);
146+
if (r != GENEVA_SUCCESS) {
147+
first_err = r;
148+
printf("Batch %zu upload failed with error %d\n", i, r);
149+
break;
150+
}
151+
}
152+
153+
/* Cleanup */
154+
geneva_batches_free(batches);
155+
geneva_free_buffer(data, data_len);
156+
geneva_client_free(client);
157+
158+
if (first_err == GENEVA_SUCCESS) {
159+
printf("All batches uploaded successfully.\n");
160+
return 0;
161+
}
162+
printf("Upload finished with error code: %d\n", first_err);
163+
return 1;
164+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[package]
2+
name = "otlp_builder"
3+
version = "0.1.0"
4+
edition = "2021"
5+
rust-version = "1.75.0"
6+
license = "Apache-2.0"
7+
8+
[lib]
9+
crate-type = ["rlib", "cdylib"]
10+
11+
[lints]
12+
workspace = true
13+
14+
[dependencies]
15+
# Use the repo's opentelemetry-proto crate directly
16+
opentelemetry-proto = { version = "0.30", default-features = false, features = ["logs", "gen-tonic-messages"] }
17+
prost = "0.13"
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
use opentelemetry_proto::tonic::collector::logs::v1::ExportLogsServiceRequest;
2+
use opentelemetry_proto::tonic::common::v1::any_value::Value as AnyValueValue;
3+
use opentelemetry_proto::tonic::common::v1::{AnyValue, KeyValue};
4+
use opentelemetry_proto::tonic::logs::v1::{LogRecord, ResourceLogs, ScopeLogs};
5+
use opentelemetry_proto::tonic::resource::v1::Resource;
6+
use prost::Message;
7+
use std::time::{SystemTime, UNIX_EPOCH};
8+
9+
/// Pure Rust helper to build a minimal OTLP ExportLogsServiceRequest as bytes.
10+
/// This is shared by the C example dylib and test-only usage via include! from lib.rs tests.
11+
///
12+
/// **Note**: This function is only intended for examples and unit tests, not for external use.
13+
pub fn build_otlp_logs_minimal(
14+
event_name: &str,
15+
body: &str,
16+
resource_kv: Option<(&str, &str)>,
17+
) -> Vec<u8> {
18+
let mut resource_attrs: Vec<KeyValue> = Vec::new();
19+
if let Some((k, v)) = resource_kv {
20+
resource_attrs.push(KeyValue {
21+
key: k.to_string(),
22+
value: Some(AnyValue {
23+
value: Some(AnyValueValue::StringValue(v.to_string())),
24+
}),
25+
});
26+
}
27+
28+
let now_nanos: u64 = SystemTime::now()
29+
.duration_since(UNIX_EPOCH)
30+
.unwrap_or_default()
31+
.as_nanos() as u64;
32+
33+
let log_record = LogRecord {
34+
time_unix_nano: now_nanos,
35+
observed_time_unix_nano: 0,
36+
severity_number: 0,
37+
severity_text: String::new(),
38+
event_name: event_name.to_string(),
39+
body: Some(AnyValue {
40+
value: Some(AnyValueValue::StringValue(body.to_string())),
41+
}),
42+
attributes: Vec::new(),
43+
dropped_attributes_count: 0,
44+
flags: 0,
45+
trace_id: Vec::new(),
46+
span_id: Vec::new(),
47+
};
48+
49+
let scope_logs = ScopeLogs {
50+
scope: None,
51+
log_records: vec![log_record],
52+
schema_url: String::new(),
53+
};
54+
55+
let resource_logs = ResourceLogs {
56+
resource: Some(Resource {
57+
attributes: resource_attrs,
58+
dropped_attributes_count: 0,
59+
entity_refs: Vec::new(),
60+
}),
61+
scope_logs: vec![scope_logs],
62+
schema_url: String::new(),
63+
};
64+
65+
let req = ExportLogsServiceRequest {
66+
resource_logs: vec![resource_logs],
67+
};
68+
69+
req.encode_to_vec()
70+
}

0 commit comments

Comments
 (0)