use std::sync::Arc;
use std::sync::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};
use tokio::runtime::Handle;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use crate::event::RunEvent;
use crate::transport::{NoopTransport, Transport};
const DEFAULT_QUEUE_SIZE: usize = 1024;
const MAX_BATCH: usize = 256;
#[derive(Debug, thiserror::Error)]
pub enum ClientError {
#[error("invalid OpenLineage configuration: {0}")]
Config(String),
}
#[derive(Debug, Clone)]
pub struct OpenLineageClient {
tx: mpsc::Sender<RunEvent>,
dropped: Arc<AtomicU64>,
drain: Arc<Mutex<Option<JoinHandle<()>>>>,
transport: Arc<dyn Transport>,
}
impl OpenLineageClient {
pub fn new(transport: Arc<dyn Transport>) -> Self {
Self::with_queue_size(transport, DEFAULT_QUEUE_SIZE)
}
pub fn try_new(transport: Arc<dyn Transport>) -> Result<Self, ClientError> {
Self::try_with_queue_size(transport, DEFAULT_QUEUE_SIZE)
}
pub fn with_queue_size(transport: Arc<dyn Transport>, queue_size: usize) -> Self {
Self::try_with_queue_size(transport, queue_size)
.expect("OpenLineageClient must be constructed within a Tokio runtime")
}
pub fn try_with_queue_size(
transport: Arc<dyn Transport>,
queue_size: usize,
) -> Result<Self, ClientError> {
let handle = Handle::try_current().map_err(|_| {
ClientError::Config(
"OpenLineageClient must be constructed within a Tokio runtime".to_string(),
)
})?;
let (tx, mut rx) = mpsc::channel::<RunEvent>(queue_size);
let dropped = Arc::new(AtomicU64::new(0));
let drain_dropped = dropped.clone();
let drain_transport = transport.clone();
let drain = handle.spawn(async move {
let mut batch: Vec<RunEvent> = Vec::new();
while let Some(first) = rx.recv().await {
batch.clear();
batch.push(first);
while batch.len() < MAX_BATCH {
match rx.try_recv() {
Ok(event) => batch.push(event),
Err(_) => break,
}
}
if let Err(err) = drain_transport.emit_batch(&batch).await {
let n = drain_dropped.fetch_add(batch.len() as u64, Ordering::Relaxed)
+ batch.len() as u64;
tracing::warn!(
target: "openlineage",
error = %err,
batch = batch.len(),
dropped_total = n,
"failed to emit lineage events; dropping batch"
);
}
}
});
Ok(Self {
tx,
dropped,
drain: Arc::new(Mutex::new(Some(drain))),
transport,
})
}
pub fn builder() -> OpenLineageClientBuilder {
OpenLineageClientBuilder::default()
}
pub fn noop() -> Self {
Self::new(Arc::new(NoopTransport))
}
pub fn from_env() -> Result<Self, ClientError> {
match std::env::var("OPENLINEAGE_URL") {
Ok(url) if !url.is_empty() => Self::http_from_env(&url),
_ => Ok(Self::noop()),
}
}
#[cfg(feature = "http")]
fn http_from_env(url: &str) -> Result<Self, ClientError> {
use crate::cloud::CloudClientTransport;
let endpoint =
std::env::var("OPENLINEAGE_ENDPOINT").unwrap_or_else(|_| "/api/v1/lineage".to_string());
let full = url.trim_end_matches('/').to_string() + &endpoint;
let endpoint_url = url::Url::parse(&full)
.map_err(|e| ClientError::Config(format!("invalid OPENLINEAGE_URL/ENDPOINT: {e}")))?;
let timeout = crate::config::OpenLineageConfig::from_env().request_timeout;
let cloud = match std::env::var("OPENLINEAGE_API_KEY") {
Ok(token) if !token.is_empty() => CloudClientTransport::with_token(endpoint_url, token),
_ => CloudClientTransport::unauthenticated(endpoint_url),
}
.with_timeout(timeout);
Ok(Self::new(Arc::new(cloud)))
}
#[cfg(not(feature = "http"))]
fn http_from_env(_url: &str) -> Result<Self, ClientError> {
Err(ClientError::Config(
"OPENLINEAGE_URL is set but the `http` feature is disabled".to_string(),
))
}
pub fn emit(&self, event: RunEvent) {
if let Err(err) = self.tx.try_send(event) {
let n = self.dropped.fetch_add(1, Ordering::Relaxed) + 1;
tracing::warn!(
target: "openlineage",
error = %err,
dropped_total = n,
"lineage queue full or closed; dropping event"
);
}
}
pub fn dropped_count(&self) -> u64 {
self.dropped.load(Ordering::Relaxed)
}
pub async fn shutdown(self) {
let Self {
tx,
drain,
transport,
..
} = self;
drop(tx);
let handle = drain.lock().unwrap().take();
if let Some(handle) = handle {
let _ = handle.await;
}
if let Err(err) = transport.flush().await {
tracing::warn!(
target: "openlineage",
error = %err,
"transport flush failed during shutdown",
);
}
}
}
#[derive(Default)]
pub struct OpenLineageClientBuilder {
transport: Option<Arc<dyn Transport>>,
queue_size: Option<usize>,
}
impl OpenLineageClientBuilder {
pub fn transport(mut self, transport: Arc<dyn Transport>) -> Self {
self.transport = Some(transport);
self
}
pub fn queue_size(mut self, queue_size: usize) -> Self {
self.queue_size = Some(queue_size);
self
}
pub fn build(self) -> OpenLineageClient {
let transport = self.transport.unwrap_or_else(|| Arc::new(NoopTransport));
OpenLineageClient::with_queue_size(transport, self.queue_size.unwrap_or(DEFAULT_QUEUE_SIZE))
}
}