use async_trait::async_trait;
use crate::{error::Result, listener::RoundReport};
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct AssetEvent {
pub uri: String,
pub extra: serde_json::Value,
}
#[async_trait]
pub trait AssetEmitter: Send + Sync {
async fn emit(&self, events: &[AssetEvent]) -> Result<()>;
}
pub fn round_to_events(uri_prefix: &str, report: &RoundReport) -> Vec<AssetEvent> {
report
.synced
.iter()
.map(|(table, rows, sink)| AssetEvent {
uri: format!("{uri_prefix}{table}"),
extra: serde_json::json!({
"table": table,
"rows": rows,
"vertices_written": sink.vertices_written,
"edges_written": sink.edges_written,
}),
})
.collect()
}
pub fn asset_event_body(event: &AssetEvent) -> serde_json::Value {
serde_json::json!({
"asset_uri": event.uri,
"extra": event.extra,
})
}
pub async fn emit_round(
emitter: &impl AssetEmitter,
uri_prefix: &str,
report: &RoundReport,
) -> Result<()> {
let events = round_to_events(uri_prefix, report);
if events.is_empty() {
return Ok(());
}
emitter.emit(&events).await
}
#[cfg(feature = "airflow")]
pub use http::AirflowEmitter;
#[cfg(feature = "airflow")]
mod http {
use super::*;
use crate::error::BifrostError;
pub struct AirflowEmitter {
client: reqwest::Client,
events_url: String,
token: Option<String>,
}
impl AirflowEmitter {
pub fn new(base_url: impl AsRef<str>, token: Option<String>) -> Self {
let base = base_url.as_ref().trim_end_matches('/');
Self {
client: reqwest::Client::new(),
events_url: format!("{base}/api/v2/assets/events"),
token,
}
}
}
#[async_trait]
impl AssetEmitter for AirflowEmitter {
async fn emit(&self, events: &[AssetEvent]) -> Result<()> {
for event in events {
let mut req = self
.client
.post(&self.events_url)
.json(&asset_event_body(event));
if let Some(token) = &self.token {
req = req.bearer_auth(token);
}
let resp = req
.send()
.await
.map_err(|e| BifrostError::Source(format!("airflow emit: {e}")))?;
if !resp.status().is_success() {
return Err(BifrostError::Source(format!(
"airflow emit: {} for {}",
resp.status(),
event.uri
)));
}
}
Ok(())
}
}
}