use std::ffi::{c_char, c_int, c_void};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use arrow_array::ffi::{FFI_ArrowArray, FFI_ArrowSchema};
use arrow_array::{Array, RecordBatch, StructArray};
use prost::Message as _;
use tokio_util::sync::CancellationToken;
use tokio_util::task::TaskTracker;
use tonic::metadata::MetadataMap;
use crate::ffi::OtlpSignalType;
use crate::otap::grpc_service::{
ArrowLogsService, ArrowLogsServiceServer, ArrowMetricsService, ArrowMetricsServiceServer,
ArrowTracesService, ArrowTracesServiceServer,
};
use crate::otap::{BatchArrowRecords, BatchStatus, OtapDecoder, StatusCode};
use opentelemetry_proto::tonic::collector::logs::v1::{
logs_service_server::{LogsService, LogsServiceServer},
ExportLogsServiceRequest, ExportLogsServiceResponse,
};
use opentelemetry_proto::tonic::collector::metrics::v1::{
metrics_service_server::{MetricsService, MetricsServiceServer},
ExportMetricsServiceRequest, ExportMetricsServiceResponse,
};
use opentelemetry_proto::tonic::collector::trace::v1::{
trace_service_server::{TraceService, TraceServiceServer},
ExportTraceServiceRequest, ExportTraceServiceResponse,
};
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OtlpIngestStatus {
Ok = 0,
ResourceExhausted = 1,
Invalid = 2,
Internal = 3,
Unauthenticated = 4,
}
pub type OtlpBatchCallback = extern "C" fn(
user_data: *mut c_void,
signal_type: OtlpSignalType,
stream_id: u64,
batch_id: i64,
input_bytes: u64,
array: *mut FFI_ArrowArray,
schema: *mut FFI_ArrowSchema,
) -> OtlpIngestStatus;
pub type OtlpAuthCallback =
extern "C" fn(user_data: *mut c_void, metadata_token: *const c_char, len: usize) -> c_int;
struct CallbackCtx {
user_data: *mut c_void,
on_batch: OtlpBatchCallback,
on_auth: Option<OtlpAuthCallback>,
}
unsafe impl Send for CallbackCtx {}
unsafe impl Sync for CallbackCtx {}
impl CallbackCtx {
fn auth_ok(&self, metadata: &MetadataMap) -> bool {
let Some(cb) = self.on_auth else {
return true;
};
let value = metadata
.get("authorization")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
cb(self.user_data, value.as_ptr() as *const c_char, value.len()) != 0
}
fn deliver(
&self,
signal_type: OtlpSignalType,
stream_id: u64,
batch_id: i64,
input_bytes: u64,
batch: RecordBatch,
) -> OtlpIngestStatus {
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let ffi_schema = match FFI_ArrowSchema::try_from(batch.schema().as_ref()) {
Ok(schema) => schema,
Err(_) => return OtlpIngestStatus::Internal,
};
let struct_array: StructArray = batch.into();
let mut ffi_array = FFI_ArrowArray::new(&struct_array.into_data());
let mut ffi_schema = ffi_schema;
(self.on_batch)(
self.user_data,
signal_type,
stream_id,
batch_id,
input_bytes,
&mut ffi_array as *mut FFI_ArrowArray,
&mut ffi_schema as *mut FFI_ArrowSchema,
)
}));
outcome.unwrap_or(OtlpIngestStatus::Internal)
}
}
impl OtlpIngestStatus {
fn detail(self) -> (StatusCode, &'static str) {
match self {
OtlpIngestStatus::Ok => (StatusCode::Ok, ""),
OtlpIngestStatus::ResourceExhausted => {
(StatusCode::ResourceExhausted, "ingest buffer full")
}
OtlpIngestStatus::Invalid => (StatusCode::InvalidArgument, "invalid payload"),
OtlpIngestStatus::Internal => (StatusCode::Internal, "ingest error"),
OtlpIngestStatus::Unauthenticated => (StatusCode::Unauthenticated, "unauthorized"),
}
}
}
fn ingest_status_to_grpc(status: OtlpIngestStatus) -> Result<(), tonic::Status> {
let msg = status.detail().1;
match status {
OtlpIngestStatus::Ok => Ok(()),
OtlpIngestStatus::ResourceExhausted => Err(tonic::Status::resource_exhausted(msg)),
OtlpIngestStatus::Invalid => Err(tonic::Status::invalid_argument(msg)),
OtlpIngestStatus::Internal => Err(tonic::Status::internal(msg)),
OtlpIngestStatus::Unauthenticated => Err(tonic::Status::unauthenticated(msg)),
}
}
struct LogsServiceImpl {
ctx: Arc<CallbackCtx>,
}
#[tonic::async_trait]
impl LogsService for LogsServiceImpl {
async fn export(
&self,
request: tonic::Request<ExportLogsServiceRequest>,
) -> Result<tonic::Response<ExportLogsServiceResponse>, tonic::Status> {
if !self.ctx.auth_ok(request.metadata()) {
return Err(tonic::Status::unauthenticated("invalid token"));
}
let req = request.into_inner();
let input_bytes = req.encoded_len();
let batch = crate::batch::transform_logs_request_observed(req, input_bytes, &mut None)
.map_err(|e| {
tonic::Status::invalid_argument(format!("failed to transform logs: {e}"))
})?;
ingest_status_to_grpc(self.ctx.deliver(
OtlpSignalType::Logs,
0,
0,
input_bytes as u64,
batch,
))?;
Ok(tonic::Response::new(ExportLogsServiceResponse::default()))
}
}
struct TraceServiceImpl {
ctx: Arc<CallbackCtx>,
}
#[tonic::async_trait]
impl TraceService for TraceServiceImpl {
async fn export(
&self,
request: tonic::Request<ExportTraceServiceRequest>,
) -> Result<tonic::Response<ExportTraceServiceResponse>, tonic::Status> {
if !self.ctx.auth_ok(request.metadata()) {
return Err(tonic::Status::unauthenticated("invalid token"));
}
let req = request.into_inner();
let input_bytes = req.encoded_len();
let batch = crate::batch::transform_traces_request_observed(req, input_bytes, &mut None)
.map_err(|e| {
tonic::Status::invalid_argument(format!("failed to transform traces: {e}"))
})?;
ingest_status_to_grpc(self.ctx.deliver(
OtlpSignalType::Traces,
0,
0,
input_bytes as u64,
batch,
))?;
Ok(tonic::Response::new(ExportTraceServiceResponse::default()))
}
}
struct MetricsServiceImpl {
ctx: Arc<CallbackCtx>,
}
#[tonic::async_trait]
impl MetricsService for MetricsServiceImpl {
async fn export(
&self,
request: tonic::Request<ExportMetricsServiceRequest>,
) -> Result<tonic::Response<ExportMetricsServiceResponse>, tonic::Status> {
if !self.ctx.auth_ok(request.metadata()) {
return Err(tonic::Status::unauthenticated("invalid token"));
}
let req = request.into_inner();
let input_bytes = req.encoded_len() as u64;
let batches =
crate::batch::transform_metrics_request_observed(req, &mut None).map_err(|e| {
tonic::Status::invalid_argument(format!("failed to transform metrics: {e}"))
})?;
ingest_status_to_grpc(deliver_metric_batches(
&self.ctx,
0,
0,
input_bytes,
batches,
))?;
Ok(tonic::Response::new(ExportMetricsServiceResponse::default()))
}
}
fn deliver_metric_batches(
ctx: &CallbackCtx,
stream_id: u64,
batch_id: i64,
input_bytes: u64,
batches: crate::MetricBatches,
) -> OtlpIngestStatus {
let mut remaining = input_bytes;
for (signal, maybe_batch) in [
(OtlpSignalType::MetricsGauge, batches.gauge),
(OtlpSignalType::MetricsSum, batches.sum),
(OtlpSignalType::MetricsHistogram, batches.histogram),
(OtlpSignalType::MetricsExpHistogram, batches.exp_histogram),
] {
if let Some(batch) = maybe_batch {
let charge = std::mem::take(&mut remaining);
let status = ctx.deliver(signal, stream_id, batch_id, charge, batch);
if status != OtlpIngestStatus::Ok {
return status;
}
}
}
OtlpIngestStatus::Ok
}
fn ingest_to_batch_status(batch_id: i64, status: OtlpIngestStatus) -> BatchStatus {
let (code, message) = status.detail();
BatchStatus {
batch_id,
status_code: code as i32,
status_message: message.to_string(),
}
}
type BatchStatusStream = tokio_stream::wrappers::ReceiverStream<Result<BatchStatus, tonic::Status>>;
type DecodeOne = fn(&mut OtapDecoder, BatchArrowRecords, usize) -> crate::Result<RecordBatch>;
fn decode_error_status(batch_id: i64, error: &crate::Error) -> BatchStatus {
BatchStatus {
batch_id,
status_code: StatusCode::InvalidArgument as i32,
status_message: format!("OTAP decode error: {error}"),
}
}
fn spawn_single_shape_stream(
ctx: Arc<CallbackCtx>,
cancel: CancellationToken,
tracker: &TaskTracker,
stream_id: u64,
signal: OtlpSignalType,
decode: DecodeOne,
mut input: tonic::Streaming<BatchArrowRecords>,
) -> BatchStatusStream {
let (tx, rx) = tokio::sync::mpsc::channel::<Result<BatchStatus, tonic::Status>>(64);
tracker.spawn(async move {
let mut decoder = OtapDecoder::new();
loop {
let message = tokio::select! {
biased;
() = cancel.cancelled() => break,
message = input.message() => message,
};
match message {
Ok(Some(envelope)) => {
let batch_id = envelope.batch_id;
let input_bytes = envelope.encoded_len();
match decode(&mut decoder, envelope, input_bytes) {
Ok(batch) => {
let ingest =
ctx.deliver(signal, stream_id, batch_id, input_bytes as u64, batch);
if tx
.send(Ok(ingest_to_batch_status(batch_id, ingest)))
.await
.is_err()
{
break; }
}
Err(error) => {
let _ = tx.send(Ok(decode_error_status(batch_id, &error))).await;
break;
}
}
}
Ok(None) => break, Err(status) => {
let _ = tx.send(Err(status)).await;
break;
}
}
}
});
tokio_stream::wrappers::ReceiverStream::new(rx)
}
fn spawn_metrics_stream(
ctx: Arc<CallbackCtx>,
cancel: CancellationToken,
tracker: &TaskTracker,
stream_id: u64,
mut input: tonic::Streaming<BatchArrowRecords>,
) -> BatchStatusStream {
let (tx, rx) = tokio::sync::mpsc::channel::<Result<BatchStatus, tonic::Status>>(64);
tracker.spawn(async move {
let mut decoder = OtapDecoder::new();
loop {
let message = tokio::select! {
biased;
() = cancel.cancelled() => break,
message = input.message() => message,
};
match message {
Ok(Some(envelope)) => {
let batch_id = envelope.batch_id;
let input_bytes = envelope.encoded_len() as u64;
match decoder.decode_metrics_message(envelope) {
Ok(batches) => {
let ingest = deliver_metric_batches(
&ctx,
stream_id,
batch_id,
input_bytes,
batches,
);
if tx
.send(Ok(ingest_to_batch_status(batch_id, ingest)))
.await
.is_err()
{
break;
}
}
Err(error) => {
let _ = tx.send(Ok(decode_error_status(batch_id, &error))).await;
break;
}
}
}
Ok(None) => break,
Err(status) => {
let _ = tx.send(Err(status)).await;
break;
}
}
}
});
tokio_stream::wrappers::ReceiverStream::new(rx)
}
struct ArrowLogsServiceImpl {
ctx: Arc<CallbackCtx>,
stream_counter: AtomicU64,
cancel: CancellationToken,
tracker: TaskTracker,
}
#[tonic::async_trait]
impl ArrowLogsService for ArrowLogsServiceImpl {
type ArrowLogsStream = BatchStatusStream;
async fn arrow_logs(
&self,
request: tonic::Request<tonic::Streaming<BatchArrowRecords>>,
) -> Result<tonic::Response<Self::ArrowLogsStream>, tonic::Status> {
if !self.ctx.auth_ok(request.metadata()) {
return Err(tonic::Status::unauthenticated("invalid token"));
}
let stream_id = self.stream_counter.fetch_add(1, Ordering::Relaxed) + 1;
Ok(tonic::Response::new(spawn_single_shape_stream(
self.ctx.clone(),
self.cancel.clone(),
&self.tracker,
stream_id,
OtlpSignalType::Logs,
OtapDecoder::decode_logs_message,
request.into_inner(),
)))
}
}
struct ArrowTracesServiceImpl {
ctx: Arc<CallbackCtx>,
stream_counter: AtomicU64,
cancel: CancellationToken,
tracker: TaskTracker,
}
#[tonic::async_trait]
impl ArrowTracesService for ArrowTracesServiceImpl {
type ArrowTracesStream = BatchStatusStream;
async fn arrow_traces(
&self,
request: tonic::Request<tonic::Streaming<BatchArrowRecords>>,
) -> Result<tonic::Response<Self::ArrowTracesStream>, tonic::Status> {
if !self.ctx.auth_ok(request.metadata()) {
return Err(tonic::Status::unauthenticated("invalid token"));
}
let stream_id = self.stream_counter.fetch_add(1, Ordering::Relaxed) + 1;
Ok(tonic::Response::new(spawn_single_shape_stream(
self.ctx.clone(),
self.cancel.clone(),
&self.tracker,
stream_id,
OtlpSignalType::Traces,
OtapDecoder::decode_traces_message,
request.into_inner(),
)))
}
}
struct ArrowMetricsServiceImpl {
ctx: Arc<CallbackCtx>,
stream_counter: AtomicU64,
cancel: CancellationToken,
tracker: TaskTracker,
}
#[tonic::async_trait]
impl ArrowMetricsService for ArrowMetricsServiceImpl {
type ArrowMetricsStream = BatchStatusStream;
async fn arrow_metrics(
&self,
request: tonic::Request<tonic::Streaming<BatchArrowRecords>>,
) -> Result<tonic::Response<Self::ArrowMetricsStream>, tonic::Status> {
if !self.ctx.auth_ok(request.metadata()) {
return Err(tonic::Status::unauthenticated("invalid token"));
}
let stream_id = self.stream_counter.fetch_add(1, Ordering::Relaxed) + 1;
Ok(tonic::Response::new(spawn_metrics_stream(
self.ctx.clone(),
self.cancel.clone(),
&self.tracker,
stream_id,
request.into_inner(),
)))
}
}
pub struct OtlpGrpcServer {
runtime: Option<tokio::runtime::Runtime>,
shutdown_tx: Option<tokio::sync::oneshot::Sender<()>>,
join: Option<tokio::task::JoinHandle<Result<(), tonic::transport::Error>>>,
cancel: CancellationToken,
tracker: TaskTracker,
}
fn write_err(err_buf: *mut c_char, err_buf_len: usize, msg: &str) {
if err_buf.is_null() || err_buf_len == 0 {
return;
}
let bytes = msg.as_bytes();
let n = bytes.len().min(err_buf_len - 1);
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr() as *const c_char, err_buf, n);
*err_buf.add(n) = 0;
}
}
fn worker_threads() -> usize {
std::thread::available_parallelism()
.map(|n| n.get().clamp(1, 4))
.unwrap_or(2)
}
#[no_mangle]
pub unsafe extern "C" fn otlp_grpc_server_start(
bind_addr: *const c_char,
bind_addr_len: usize,
on_batch: Option<OtlpBatchCallback>,
on_auth: Option<OtlpAuthCallback>,
user_data: *mut c_void,
max_decoding_message_bytes: u64,
err_buf: *mut c_char,
err_buf_len: usize,
) -> *mut OtlpGrpcServer {
let Some(on_batch) = on_batch else {
write_err(err_buf, err_buf_len, "on_batch callback is null");
return std::ptr::null_mut();
};
if bind_addr.is_null() {
write_err(err_buf, err_buf_len, "bind_addr is null");
return std::ptr::null_mut();
}
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let addr_slice = std::slice::from_raw_parts(bind_addr as *const u8, bind_addr_len);
let addr = std::str::from_utf8(addr_slice)
.map_err(|_| "bind_addr is not valid UTF-8".to_string())?;
let ctx = Arc::new(CallbackCtx {
user_data,
on_batch,
on_auth,
});
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(worker_threads())
.thread_name("otlp-grpc")
.enable_all()
.build()
.map_err(|e| format!("failed to start runtime: {e}"))?;
let std_listener =
std::net::TcpListener::bind(addr).map_err(|e| format!("failed to bind {addr}: {e}"))?;
std_listener
.set_nonblocking(true)
.map_err(|e| format!("failed to configure listener: {e}"))?;
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
let cancel = CancellationToken::new();
let tracker = TaskTracker::new();
const TONIC_DEFAULT_MAX_DECODING: usize = 4 * 1024 * 1024;
let max_decoding = if max_decoding_message_bytes == 0 {
TONIC_DEFAULT_MAX_DECODING
} else {
max_decoding_message_bytes as usize
};
let logs = LogsServiceServer::new(LogsServiceImpl { ctx: ctx.clone() })
.max_decoding_message_size(max_decoding);
let traces = TraceServiceServer::new(TraceServiceImpl { ctx: ctx.clone() })
.max_decoding_message_size(max_decoding);
let metrics = MetricsServiceServer::new(MetricsServiceImpl { ctx: ctx.clone() })
.max_decoding_message_size(max_decoding);
let arrow_logs = ArrowLogsServiceServer::new(ArrowLogsServiceImpl {
ctx: ctx.clone(),
stream_counter: AtomicU64::new(0),
cancel: cancel.clone(),
tracker: tracker.clone(),
})
.max_decoding_message_size(max_decoding);
let arrow_traces = ArrowTracesServiceServer::new(ArrowTracesServiceImpl {
ctx: ctx.clone(),
stream_counter: AtomicU64::new(0),
cancel: cancel.clone(),
tracker: tracker.clone(),
})
.max_decoding_message_size(max_decoding);
let arrow_metrics = ArrowMetricsServiceServer::new(ArrowMetricsServiceImpl {
ctx: ctx.clone(),
stream_counter: AtomicU64::new(0),
cancel: cancel.clone(),
tracker: tracker.clone(),
})
.max_decoding_message_size(max_decoding);
let join = {
let _guard = runtime.enter();
let tokio_listener = tokio::net::TcpListener::from_std(std_listener)
.map_err(|e| format!("failed to register listener: {e}"))?;
let incoming = tokio_stream::wrappers::TcpListenerStream::new(tokio_listener);
runtime.spawn(async move {
tonic::transport::Server::builder()
.add_service(logs)
.add_service(traces)
.add_service(metrics)
.add_service(arrow_logs)
.add_service(arrow_traces)
.add_service(arrow_metrics)
.serve_with_incoming_shutdown(incoming, async move {
let _ = shutdown_rx.await;
})
.await
})
};
Ok::<_, String>(OtlpGrpcServer {
runtime: Some(runtime),
shutdown_tx: Some(shutdown_tx),
join: Some(join),
cancel,
tracker,
})
}));
match result {
Ok(Ok(server)) => Box::into_raw(Box::new(server)),
Ok(Err(msg)) => {
write_err(err_buf, err_buf_len, &msg);
std::ptr::null_mut()
}
Err(_) => {
write_err(err_buf, err_buf_len, "panic while starting gRPC server");
std::ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn otlp_grpc_server_stop(
server: *mut OtlpGrpcServer,
drain_deadline_ms: u64,
) {
if server.is_null() {
return;
}
let server = &mut *server;
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let deadline = std::time::Duration::from_millis(drain_deadline_ms.max(1));
let start = std::time::Instant::now();
if let Some(tx) = server.shutdown_tx.take() {
let _ = tx.send(());
}
server.cancel.cancel();
server.tracker.close();
if let Some(runtime) = server.runtime.as_ref() {
let join = server.join.take();
let tracker = server.tracker.clone();
runtime.block_on(async move {
let _ = tokio::time::timeout(deadline, async move {
if let Some(join) = join {
let _ = join.await;
}
tracker.wait().await;
})
.await;
});
}
if let Some(runtime) = server.runtime.take() {
let remaining = deadline
.saturating_sub(start.elapsed())
.max(std::time::Duration::from_millis(1));
runtime.shutdown_timeout(remaining);
}
}));
}
#[no_mangle]
pub unsafe extern "C" fn otlp_grpc_server_free(server: *mut OtlpGrpcServer) {
if !server.is_null() {
drop(Box::from_raw(server));
}
}