openlineage_client/
cloud.rs1use std::time::Duration;
10
11use async_trait::async_trait;
12use olai_http::CloudClient;
13use url::Url;
14
15use crate::config::DEFAULT_REQUEST_TIMEOUT;
16use crate::event::RunEvent;
17use crate::transport::{Transport, TransportError};
18
19#[derive(Clone)]
24pub struct CloudClientTransport {
25 client: CloudClient,
26 endpoint: Url,
27 batch_endpoint: Url,
28 timeout: Duration,
29}
30
31impl std::fmt::Debug for CloudClientTransport {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 f.debug_struct("CloudClientTransport")
35 .field("endpoint", &self.endpoint)
36 .field("batch_endpoint", &self.batch_endpoint)
37 .field("timeout", &self.timeout)
38 .finish_non_exhaustive()
39 }
40}
41
42impl CloudClientTransport {
43 pub fn new(client: CloudClient, endpoint: Url) -> Self {
50 let batch_endpoint = default_batch_endpoint(&endpoint);
51 Self {
52 client,
53 endpoint,
54 batch_endpoint,
55 timeout: DEFAULT_REQUEST_TIMEOUT,
56 }
57 }
58
59 pub fn with_token(endpoint: Url, token: impl ToString) -> Self {
61 Self::new(CloudClient::new_with_token(token), endpoint)
62 }
63
64 pub fn unauthenticated(endpoint: Url) -> Self {
66 Self::new(CloudClient::new_unauthenticated(), endpoint)
67 }
68
69 pub fn with_timeout(mut self, timeout: Duration) -> Self {
71 self.timeout = timeout;
72 self
73 }
74
75 pub fn with_batch_endpoint(mut self, batch_endpoint: Url) -> Self {
77 self.batch_endpoint = batch_endpoint;
78 self
79 }
80}
81
82fn default_batch_endpoint(endpoint: &Url) -> Url {
85 let mut batch = endpoint.clone();
86 if let Ok(mut segments) = batch.path_segments_mut() {
87 segments.pop_if_empty().push("batch");
88 }
89 batch
90}
91
92#[async_trait]
93impl Transport for CloudClientTransport {
94 async fn emit(&self, event: &RunEvent) -> Result<(), TransportError> {
95 self.client
96 .post(self.endpoint.clone())
97 .timeout(self.timeout)
98 .json(event)
99 .send()
100 .await
101 .map_err(|e| TransportError::Other(e.to_string()))?;
102 Ok(())
103 }
104
105 async fn emit_batch(&self, events: &[RunEvent]) -> Result<(), TransportError> {
106 match events {
108 [] => Ok(()),
109 [event] => self.emit(event).await,
110 events => {
111 self.client
112 .post(self.batch_endpoint.clone())
113 .timeout(self.timeout)
114 .json(events)
115 .send()
116 .await
117 .map_err(|e| TransportError::Other(e.to_string()))?;
118 Ok(())
119 }
120 }
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn batch_endpoint_appends_batch_segment() {
130 let endpoint = Url::parse("http://localhost:8091/api/v1/lineage").unwrap();
131 let batch = default_batch_endpoint(&endpoint);
132 assert_eq!(batch.as_str(), "http://localhost:8091/api/v1/lineage/batch");
133 }
134
135 #[test]
136 fn batch_endpoint_handles_trailing_slash() {
137 let endpoint = Url::parse("http://localhost:8091/api/v1/lineage/").unwrap();
138 let batch = default_batch_endpoint(&endpoint);
139 assert_eq!(batch.as_str(), "http://localhost:8091/api/v1/lineage/batch");
140 }
141
142 fn endpoint() -> Url {
143 Url::parse("http://localhost:8091/api/v1/lineage").unwrap()
144 }
145
146 #[test]
147 fn new_defaults_batch_endpoint_and_timeout() {
148 let t = CloudClientTransport::unauthenticated(endpoint());
149 assert_eq!(t.endpoint.as_str(), "http://localhost:8091/api/v1/lineage");
150 assert_eq!(
151 t.batch_endpoint.as_str(),
152 "http://localhost:8091/api/v1/lineage/batch",
153 "batch endpoint defaults to endpoint + /batch"
154 );
155 assert_eq!(t.timeout, DEFAULT_REQUEST_TIMEOUT);
156 }
157
158 #[test]
159 fn with_token_carries_the_same_endpoint_defaults() {
160 let t = CloudClientTransport::with_token(endpoint(), "secret");
161 assert_eq!(
162 t.batch_endpoint.as_str(),
163 "http://localhost:8091/api/v1/lineage/batch"
164 );
165 assert_eq!(t.timeout, DEFAULT_REQUEST_TIMEOUT);
166 }
167
168 #[test]
169 fn with_timeout_and_with_batch_endpoint_override_defaults() {
170 let custom_batch = Url::parse("http://localhost:8091/bulk").unwrap();
171 let t = CloudClientTransport::unauthenticated(endpoint())
172 .with_timeout(Duration::from_secs(5))
173 .with_batch_endpoint(custom_batch.clone());
174 assert_eq!(t.timeout, Duration::from_secs(5));
175 assert_eq!(t.batch_endpoint, custom_batch);
176 assert_eq!(t.endpoint, endpoint());
178 }
179
180 #[tokio::test]
181 async fn emit_batch_of_nothing_is_a_no_op() {
182 let t = CloudClientTransport::unauthenticated(endpoint());
185 assert!(t.emit_batch(&[]).await.is_ok());
186 }
187}