saluki_components/relays/otlp/
mod.rs1use std::sync::LazyLock;
2
3use agent_data_plane_config::domains;
4use async_trait::async_trait;
5use axum::body::Bytes;
6use saluki_common::buf::FrozenChunkedBytesBuffer;
7use saluki_core::accounting::{MemoryBounds, MemoryBoundsBuilder};
8use saluki_core::components::relays::{Relay, RelayBuilder, RelayContext};
9use saluki_core::components::ComponentContext;
10use saluki_core::data_model::payload::{GrpcPayload, Payload, PayloadMetadata, PayloadType};
11use saluki_core::topology::OutputDefinition;
12use saluki_error::{ErrorContext as _, GenericError};
13use saluki_io::net::ListenAddress;
14use stringtheory::MetaString;
15use tokio::sync::mpsc;
16use tokio::{pin, select};
17use tracing::{debug, error};
18
19use crate::common::otlp::{
20 build_metrics, CorsConfiguration, Metrics, OtlpHandler, OtlpServerBuilder, OTLP_LOGS_GRPC_SERVICE_PATH,
21 OTLP_METRICS_GRPC_SERVICE_PATH, OTLP_TRACES_GRPC_SERVICE_PATH,
22};
23
24fn cors_configuration(cors: &domains::otlp::Cors) -> CorsConfiguration {
26 CorsConfiguration {
27 allowed_origins: cors.allowed_origins.clone(),
28 allowed_headers: cors.allowed_headers.clone(),
29 exposed_headers: cors.exposed_headers.clone(),
30 max_age: cors.max_age,
31 }
32}
33
34#[derive(Default)]
36pub struct OtlpRelayConfiguration {
37 receiver: domains::otlp::Receiver,
38}
39
40impl OtlpRelayConfiguration {
41 pub fn from_configuration(receiver: &domains::otlp::Receiver) -> Self {
43 Self {
44 receiver: receiver.clone(),
45 }
46 }
47
48 fn http_endpoint(&self) -> ListenAddress {
49 let address = format!("{}://{}", self.receiver.http.transport, self.receiver.http.endpoint);
50 ListenAddress::try_from(address).expect("valid HTTP endpoint")
51 }
52
53 fn grpc_endpoint(&self) -> ListenAddress {
54 let address = format!(
55 "{}://{}",
56 self.receiver.grpc.transport.as_str(),
57 self.receiver.grpc.endpoint
58 );
59 ListenAddress::try_from(address).expect("valid gRPC endpoint")
60 }
61
62 fn grpc_max_recv_msg_size_bytes(&self) -> usize {
63 (self.receiver.grpc.max_recv_msg_size_mib * 1024 * 1024) as usize
64 }
65}
66
67impl MemoryBounds for OtlpRelayConfiguration {
68 fn specify_bounds(&self, _builder: &mut MemoryBoundsBuilder) {}
69}
70
71#[async_trait]
72impl RelayBuilder for OtlpRelayConfiguration {
73 fn outputs(&self) -> &[OutputDefinition<PayloadType>] {
74 static OUTPUTS: LazyLock<Vec<OutputDefinition<PayloadType>>> = LazyLock::new(|| {
75 vec![
76 OutputDefinition::named_output("metrics", PayloadType::Grpc),
77 OutputDefinition::named_output("logs", PayloadType::Grpc),
78 OutputDefinition::named_output("traces", PayloadType::Grpc),
79 ]
80 });
81 &OUTPUTS
82 }
83
84 async fn build(&self, context: ComponentContext) -> Result<Box<dyn Relay + Send>, GenericError> {
85 Ok(Box::new(OtlpRelay {
86 http_endpoint: self.http_endpoint(),
87 grpc_endpoint: self.grpc_endpoint(),
88 grpc_max_recv_msg_size_bytes: self.grpc_max_recv_msg_size_bytes(),
89 cors: cors_configuration(&self.receiver.http.cors),
90 metrics: build_metrics(&context),
91 }))
92 }
93}
94
95pub struct OtlpRelay {
99 http_endpoint: ListenAddress,
100 grpc_endpoint: ListenAddress,
101 grpc_max_recv_msg_size_bytes: usize,
102 cors: CorsConfiguration,
103 metrics: Metrics,
104}
105
106#[async_trait]
107impl Relay for OtlpRelay {
108 async fn run(self: Box<Self>, mut context: RelayContext) -> Result<(), GenericError> {
109 let Self {
110 http_endpoint,
111 grpc_endpoint,
112 grpc_max_recv_msg_size_bytes,
113 cors,
114 metrics,
115 } = *self;
116
117 let global_shutdown = context.take_shutdown_handle();
118 pin!(global_shutdown);
119
120 let mut health = context.take_health_handle();
121 let memory_limiter = context.topology_context().memory_limiter().clone();
122
123 let (payload_tx, mut payload_rx) = mpsc::channel(1024);
124
125 let handler = RelayHandler::new(payload_tx);
127 let server_builder = OtlpServerBuilder::new(
128 http_endpoint.clone(),
129 grpc_endpoint.clone(),
130 grpc_max_recv_msg_size_bytes,
131 )
132 .with_cors(cors);
133
134 server_builder
135 .build(handler, memory_limiter, metrics, context.spawner())
136 .await?;
137
138 health.mark_ready();
139 debug!(%http_endpoint, %grpc_endpoint, "OTLP relay started.");
140
141 loop {
142 select! {
143 _ = &mut global_shutdown => {
144 debug!("Received shutdown signal.");
145 break
146 },
147 Some(otlp_payload) = payload_rx.recv() => {
148 let output_name = otlp_payload.signal_type.as_str();
149 let payload = Payload::Grpc(otlp_payload.into_grpc_payload());
150 if let Err(e) = context.dispatcher().dispatch_named(output_name, payload).await {
151 error!(error = %e, output = output_name, "Failed to dispatch OTLP payload.");
152 }
153 },
154 _ = health.live() => continue,
155 }
156 }
157
158 debug!("Stopping OTLP relay...");
159 debug!("OTLP relay stopped.");
160
161 Ok(())
162 }
163}
164
165enum OtlpSignalType {
166 Metrics,
167 Logs,
168 Traces,
169}
170
171impl OtlpSignalType {
172 fn as_str(&self) -> &'static str {
173 match self {
174 OtlpSignalType::Metrics => "metrics",
175 OtlpSignalType::Logs => "logs",
176 OtlpSignalType::Traces => "traces",
177 }
178 }
179}
180
181struct OtlpPayload {
182 signal_type: OtlpSignalType,
183 data: Bytes,
184}
185
186impl OtlpPayload {
187 fn metrics(data: Bytes) -> Self {
188 Self {
189 signal_type: OtlpSignalType::Metrics,
190 data,
191 }
192 }
193
194 fn logs(data: Bytes) -> Self {
195 Self {
196 signal_type: OtlpSignalType::Logs,
197 data,
198 }
199 }
200
201 fn traces(data: Bytes) -> Self {
202 Self {
203 signal_type: OtlpSignalType::Traces,
204 data,
205 }
206 }
207
208 fn into_grpc_payload(self) -> GrpcPayload {
209 let service_path = match self.signal_type {
210 OtlpSignalType::Metrics => OTLP_METRICS_GRPC_SERVICE_PATH,
211 OtlpSignalType::Logs => OTLP_LOGS_GRPC_SERVICE_PATH,
212 OtlpSignalType::Traces => OTLP_TRACES_GRPC_SERVICE_PATH,
213 };
214
215 GrpcPayload::new(
217 PayloadMetadata::from_event_count(1),
218 MetaString::empty(),
219 service_path,
220 FrozenChunkedBytesBuffer::from(self.data),
221 )
222 }
223}
224
225struct RelayHandler {
227 tx: mpsc::Sender<OtlpPayload>,
228}
229
230impl RelayHandler {
231 fn new(tx: mpsc::Sender<OtlpPayload>) -> Self {
232 Self { tx }
233 }
234}
235
236#[async_trait]
237impl OtlpHandler for RelayHandler {
238 async fn handle_metrics(&self, body: Bytes) -> Result<(), GenericError> {
239 self.tx
240 .send(OtlpPayload::metrics(body))
241 .await
242 .error_context("Failed to send OTLP metrics payload to relay dispatcher: channel closed.")
243 }
244
245 async fn handle_logs(&self, body: Bytes) -> Result<(), GenericError> {
246 self.tx
247 .send(OtlpPayload::logs(body))
248 .await
249 .error_context("Failed to send OTLP logs payload to relay dispatcher: channel closed.")
250 }
251
252 async fn handle_traces(&self, body: Bytes) -> Result<(), GenericError> {
253 self.tx
254 .send(OtlpPayload::traces(body))
255 .await
256 .error_context("Failed to send OTLP traces payload to relay dispatcher: channel closed.")
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use agent_data_plane_config::domains;
263 use agent_data_plane_config::domains::otlp::GrpcTransport;
264
265 use super::OtlpRelayConfiguration;
266
267 fn relay(receiver: domains::otlp::Receiver) -> OtlpRelayConfiguration {
268 OtlpRelayConfiguration::from_configuration(&receiver)
269 }
270
271 #[test]
272 fn endpoints_combine_transport_and_address() {
273 let config = relay(domains::otlp::Receiver {
274 grpc: domains::otlp::GrpcReceiver {
275 endpoint: "0.0.0.0:4317".to_string(),
276 transport: GrpcTransport::Tcp,
277 max_recv_msg_size_mib: 4,
278 },
279 http: domains::otlp::HttpReceiver {
280 endpoint: "0.0.0.0:4318".to_string(),
281 transport: "tcp".to_string(),
282 cors: Default::default(),
283 },
284 ..Default::default()
285 });
286
287 assert_eq!(config.grpc_endpoint().to_string(), "tcp://0.0.0.0:4317");
288 assert_eq!(config.http_endpoint().to_string(), "tcp://0.0.0.0:4318");
289 }
290
291 #[test]
292 fn grpc_max_recv_msg_size_converts_mib_to_bytes() {
293 let config = relay(domains::otlp::Receiver {
294 grpc: domains::otlp::GrpcReceiver {
295 max_recv_msg_size_mib: 8,
296 ..Default::default()
297 },
298 ..Default::default()
299 });
300
301 assert_eq!(config.grpc_max_recv_msg_size_bytes(), 8 * 1024 * 1024);
302 }
303}