use crate::client::KibanaClient;
use crate::etl::Extractor;
use crate::{Error, Result, ResultContext};
use serde_json::Value;
use tokio::task::JoinSet;
pub struct WorkflowsExtractor {
client: KibanaClient,
manifest: Option<super::WorkflowsManifest>,
}
impl WorkflowsExtractor {
pub fn new(client: KibanaClient, manifest: Option<super::WorkflowsManifest>) -> Self {
Self { client, manifest }
}
pub async fn search_workflows(
&self,
query: Option<&str>,
size: Option<usize>,
) -> Result<Vec<Value>> {
let search_body = serde_json::json!({
"size": size.unwrap_or(100),
"query": query.unwrap_or("")
});
tracing::debug!(
"Searching workflows with query: {:?} in space '{}'",
query,
self.client.space_id()
);
let response = self
.client
.post_json_value_internal("api/workflows/search", &search_body)
.await
.context("Failed to search workflows")?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(Error::api_response(status, body));
}
let search_result: Value = response
.json()
.await
.context("Failed to parse workflow search response")?;
let workflows: Vec<Value> = search_result
.get("results")
.and_then(|v| v.as_array())
.map(|arr| arr.to_vec())
.unwrap_or_default();
tracing::info!("Found {} workflow(s) via search", workflows.len());
Ok(workflows)
}
async fn fetch_manifest_workflows(
&self,
manifest: &super::WorkflowsManifest,
) -> Result<Vec<Value>> {
let mut workflows = Vec::new();
let mut set = JoinSet::new();
for entry in &manifest.workflows {
let client = self.client.clone();
let workflow_id = entry.id.clone();
let workflow_name = entry.name.clone();
set.spawn(async move {
let path = format!("api/workflows/{}", workflow_id);
tracing::debug!(
"Fetching workflow '{}' from space '{}'",
workflow_id,
client.space_id()
);
let response = client.get_internal(&path).await.with_context(|| {
format!(
"Failed to fetch workflow '{}' ({})",
workflow_name, workflow_id
)
})?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(Error::api_response(status, body));
}
let workflow: Value = response.json().await.with_context(|| {
format!("Failed to parse workflow '{}' response", workflow_id)
})?;
tracing::debug!("Fetched workflow: {}", workflow_id);
Ok::<Value, Error>(workflow)
});
}
while let Some(res) = set.join_next().await {
match res {
Ok(Ok(workflow)) => workflows.push(workflow),
Ok(Err(e)) => tracing::warn!("{}", e),
Err(e) => tracing::error!("Task panicked: {}", e),
}
}
tracing::info!("Fetched {} workflow(s) from manifest", workflows.len());
Ok(workflows)
}
}
impl Extractor for WorkflowsExtractor {
type Item = Value;
async fn extract(&self) -> Result<Vec<Self::Item>> {
let workflows = if let Some(manifest) = &self.manifest {
self.fetch_manifest_workflows(manifest).await?
} else {
tracing::warn!("No manifest provided - use search API to discover workflows");
Vec::new()
};
tracing::info!(
"Extracted {} workflow(s){}",
workflows.len(),
if self.manifest.is_some() {
" (from manifest)"
} else {
""
}
);
Ok(workflows)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::{Auth, KibanaClient};
use url::Url;
#[test]
fn test_extractor_creation() {
let url = Url::parse("http://localhost:5601").unwrap();
let client = KibanaClient::new(url, Auth::None).unwrap();
let space_client = client.space("default").unwrap();
let _extractor = WorkflowsExtractor::new(space_client, None);
}
}