laminar_connectors/otel/
server.rs1use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::Arc;
9use std::time::Duration;
10
11use arrow_array::RecordBatch;
12use arrow_schema::SchemaRef;
13use crossfire::{mpsc, MAsyncTx};
14use tokio::sync::Notify;
15use tonic::{Request, Response, Status};
16
17use opentelemetry_proto::tonic::collector::logs::v1::logs_service_server::LogsService;
18use opentelemetry_proto::tonic::collector::logs::v1::{
19 ExportLogsServiceRequest, ExportLogsServiceResponse,
20};
21use opentelemetry_proto::tonic::collector::metrics::v1::metrics_service_server::MetricsService;
22use opentelemetry_proto::tonic::collector::metrics::v1::{
23 ExportMetricsServiceRequest, ExportMetricsServiceResponse,
24};
25use opentelemetry_proto::tonic::collector::trace::v1::trace_service_server::TraceService;
26use opentelemetry_proto::tonic::collector::trace::v1::{
27 ExportTraceServiceRequest, ExportTraceServiceResponse,
28};
29
30use crate::connector::ConnectorTaskGuard;
31
32use super::convert::{logs_request_to_batch, metrics_request_to_batch, trace_request_to_batch};
33
34const DOWNSTREAM_SEND_TIMEOUT: Duration = Duration::from_secs(5);
35
36fn now_nanos() -> i64 {
40 std::time::SystemTime::now()
41 .duration_since(std::time::UNIX_EPOCH)
42 .map_or(0, |d| {
43 #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
45 {
46 d.as_nanos() as i64
47 }
48 })
49}
50
51async fn split_and_send(
54 batch: RecordBatch,
55 batch_size: usize,
56 tx: &MAsyncTx<mpsc::Array<RecordBatch>>,
57 notify: &Notify,
58 timeout: Duration,
59) -> Result<usize, Status> {
60 let total = batch.num_rows();
61 if total == 0 {
62 return Ok(0);
63 }
64
65 let mut offset = 0;
66 while offset < total {
67 let len = (total - offset).min(batch_size);
68 let chunk = batch.slice(offset, len);
69
70 match tokio::time::timeout(timeout, tx.send(chunk)).await {
71 Ok(Ok(())) => {
72 notify.notify_one();
73 offset += len;
74 }
75 Ok(Err(_)) => {
76 return Err(Status::unavailable("OTel receiver is shutting down"));
77 }
78 Err(_) => {
79 tracing::warn!(
80 sent = offset,
81 remaining = total - offset,
82 "OTel batch send timed out — downstream backpressure"
83 );
84 return Err(Status::resource_exhausted(
85 "pipeline backpressure: retry with backoff",
86 ));
87 }
88 }
89 }
90
91 Ok(total)
92}
93
94pub struct OtelReceiver {
101 batch_tx: MAsyncTx<mpsc::Array<RecordBatch>>,
102 schema: SchemaRef,
103 data_ready: Arc<Notify>,
104 records_received: Arc<AtomicU64>,
105 requests_received: Arc<AtomicU64>,
106 send_timeout: Duration,
107 batch_size: usize,
108 _terminal_guard: ConnectorTaskGuard,
112}
113
114impl OtelReceiver {
115 pub fn new(
117 batch_tx: MAsyncTx<mpsc::Array<RecordBatch>>,
118 schema: SchemaRef,
119 data_ready: Arc<Notify>,
120 records_received: Arc<AtomicU64>,
121 requests_received: Arc<AtomicU64>,
122 batch_size: usize,
123 terminal_guard: ConnectorTaskGuard,
124 ) -> Self {
125 Self {
126 batch_tx,
127 schema,
128 data_ready,
129 records_received,
130 requests_received,
131 send_timeout: DOWNSTREAM_SEND_TIMEOUT,
132 batch_size,
133 _terminal_guard: terminal_guard,
134 }
135 }
136
137 async fn handle_batch(&self, batch: Option<RecordBatch>) -> Result<usize, Status> {
139 let Some(batch) = batch else {
140 return Ok(0);
141 };
142 let n = split_and_send(
143 batch,
144 self.batch_size,
145 &self.batch_tx,
146 &self.data_ready,
147 self.send_timeout,
148 )
149 .await?;
150 #[allow(clippy::cast_possible_truncation)]
151 self.records_received.fetch_add(n as u64, Ordering::Relaxed);
152 Ok(n)
153 }
154}
155
156#[tonic::async_trait]
159impl TraceService for OtelReceiver {
160 async fn export(
161 &self,
162 request: Request<ExportTraceServiceRequest>,
163 ) -> Result<Response<ExportTraceServiceResponse>, Status> {
164 self.requests_received.fetch_add(1, Ordering::Relaxed);
165 let batch = trace_request_to_batch(&request.into_inner(), &self.schema, now_nanos())
166 .map_err(|e| Status::internal(format!("batch conversion failed: {e}")))?;
167 self.handle_batch(batch).await?;
168 Ok(Response::new(ExportTraceServiceResponse {
169 partial_success: None,
170 }))
171 }
172}
173
174#[tonic::async_trait]
175impl MetricsService for OtelReceiver {
176 async fn export(
177 &self,
178 request: Request<ExportMetricsServiceRequest>,
179 ) -> Result<Response<ExportMetricsServiceResponse>, Status> {
180 self.requests_received.fetch_add(1, Ordering::Relaxed);
181 let batch = metrics_request_to_batch(&request.into_inner(), &self.schema, now_nanos())
182 .map_err(|e| Status::internal(format!("batch conversion failed: {e}")))?;
183 self.handle_batch(batch).await?;
184 Ok(Response::new(ExportMetricsServiceResponse {
185 partial_success: None,
186 }))
187 }
188}
189
190#[tonic::async_trait]
191impl LogsService for OtelReceiver {
192 async fn export(
193 &self,
194 request: Request<ExportLogsServiceRequest>,
195 ) -> Result<Response<ExportLogsServiceResponse>, Status> {
196 self.requests_received.fetch_add(1, Ordering::Relaxed);
197 let batch = logs_request_to_batch(&request.into_inner(), &self.schema, now_nanos())
198 .map_err(|e| Status::internal(format!("batch conversion failed: {e}")))?;
199 self.handle_batch(batch).await?;
200 Ok(Response::new(ExportLogsServiceResponse {
201 partial_success: None,
202 }))
203 }
204}