vendor: update cargo-cxcloud-output-0.1.0
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"git": {
|
||||
"sha1": "927a31cbf65f78c3ef6b729631b2fc35335afe06",
|
||||
"dirty": true
|
||||
},
|
||||
"path_in_vcs": "services/cxcloud-rs/crates/output"
|
||||
}
|
||||
Generated
+2764
File diff suppressed because it is too large
Load Diff
+85
@@ -0,0 +1,85 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2021"
|
||||
name = "cxcloud-output"
|
||||
version = "0.1.0"
|
||||
build = false
|
||||
publish = ["cxai"]
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
readme = false
|
||||
|
||||
[[bin]]
|
||||
name = "output-service"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies.anyhow]
|
||||
version = "1"
|
||||
|
||||
[dependencies.axum]
|
||||
version = "0.7"
|
||||
features = ["macros"]
|
||||
|
||||
[dependencies.chrono]
|
||||
version = "0.4"
|
||||
features = ["serde"]
|
||||
|
||||
[dependencies.cxcloud-common]
|
||||
version = "0.1.0"
|
||||
registry-index = "sparse+https://git.cxllm-studio.com/api/packages/CxAI-LLM/cargo/"
|
||||
|
||||
[dependencies.cxcloud-proto]
|
||||
version = "0.1.0"
|
||||
registry-index = "sparse+https://git.cxllm-studio.com/api/packages/CxAI-LLM/cargo/"
|
||||
|
||||
[dependencies.redis]
|
||||
version = "0.27"
|
||||
features = [
|
||||
"tokio-comp",
|
||||
"connection-manager",
|
||||
]
|
||||
|
||||
[dependencies.reqwest]
|
||||
version = "0.12"
|
||||
features = [
|
||||
"json",
|
||||
"rustls-tls",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1"
|
||||
features = ["derive"]
|
||||
|
||||
[dependencies.serde_json]
|
||||
version = "1"
|
||||
|
||||
[dependencies.thiserror]
|
||||
version = "2"
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1"
|
||||
features = ["full"]
|
||||
|
||||
[dependencies.tracing]
|
||||
version = "0.1"
|
||||
|
||||
[dependencies.uuid]
|
||||
version = "1"
|
||||
features = [
|
||||
"v7",
|
||||
"serde",
|
||||
]
|
||||
Generated
+24
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "cxcloud-output"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "output-service"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
cxcloud-common = { workspace = true }
|
||||
cxcloud-proto = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
axum = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
redis = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
@@ -1,3 +0,0 @@
|
||||
# cargo-cxcloud-output-0.1.0
|
||||
|
||||
Cargo crate: cxcloud-output-0.1.0
|
||||
@@ -0,0 +1,57 @@
|
||||
use std::sync::Arc;
|
||||
use tracing::{error, info};
|
||||
|
||||
use cxcloud_common::{event::Event, redis_streams};
|
||||
|
||||
use crate::{router, AppState};
|
||||
|
||||
/// Continuously consume from events.output and route to delivery handlers.
|
||||
pub async fn run(state: Arc<AppState>) {
|
||||
let consumer_name = format!("output-{}", uuid::Uuid::now_v7());
|
||||
info!(consumer = %consumer_name, "Output consumer starting");
|
||||
|
||||
loop {
|
||||
let entries = match redis_streams::xreadgroup(
|
||||
&state.redis,
|
||||
"output-group",
|
||||
&consumer_name,
|
||||
"events.output",
|
||||
1,
|
||||
5000,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(entries) => entries,
|
||||
Err(e) => {
|
||||
error!(error = %e, "Error reading from events.output");
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
for (msg_id, fields) in &entries {
|
||||
{
|
||||
match Event::from_stream_fields(&fields) {
|
||||
Some(event) => {
|
||||
info!(event_id = %event.id, event_type = %event.r#type, "Processing output event");
|
||||
|
||||
if let Err(e) = router::route_and_deliver(&state, &event).await {
|
||||
error!(event_id = %event.id, error = %e, "Delivery failed");
|
||||
}
|
||||
|
||||
// ACK the message
|
||||
if let Err(e) =
|
||||
redis_streams::xack(&state.redis, "events.output", "output-group", &msg_id)
|
||||
.await
|
||||
{
|
||||
error!(msg_id = %msg_id, error = %e, "Failed to ACK");
|
||||
}
|
||||
}
|
||||
None => {
|
||||
error!(msg_id = %msg_id, "Failed to parse event");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use anyhow::Result;
|
||||
use tracing::info;
|
||||
|
||||
use cxcloud_common::event::Event;
|
||||
|
||||
/// Deliver event by writing to a file.
|
||||
pub async fn deliver(event: &Event) -> Result<()> {
|
||||
let dir = event
|
||||
.metadata
|
||||
.get("output_dir")
|
||||
.map(|v| v.as_str())
|
||||
.unwrap_or("/data/output");
|
||||
|
||||
let filename = format!("{}.json", event.id);
|
||||
let path = std::path::Path::new(dir).join(&filename);
|
||||
|
||||
// Ensure directory exists
|
||||
if let Some(parent) = path.parent() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
}
|
||||
|
||||
let content = serde_json::to_string_pretty(&serde_json::json!({
|
||||
"event_id": event.id,
|
||||
"event_type": event.r#type,
|
||||
"source": event.source,
|
||||
"timestamp": event.timestamp,
|
||||
"payload": event.payload,
|
||||
}))?;
|
||||
|
||||
tokio::fs::write(&path, &content).await?;
|
||||
info!(path = %path.display(), size = content.len(), "File delivery complete");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use anyhow::{bail, Result};
|
||||
use tracing::{info, warn};
|
||||
|
||||
use cxcloud_common::event::Event;
|
||||
|
||||
const MAX_RETRIES: u32 = 3;
|
||||
|
||||
/// Deliver event via HTTP webhook POST with retries.
|
||||
pub async fn deliver(client: &reqwest::Client, event: &Event) -> Result<()> {
|
||||
let url = event
|
||||
.metadata
|
||||
.get("webhook_url")
|
||||
.map(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("No webhook_url in event metadata"))?;
|
||||
|
||||
let body = serde_json::json!({
|
||||
"event_id": event.id,
|
||||
"event_type": event.r#type,
|
||||
"source": event.source,
|
||||
"timestamp": event.timestamp,
|
||||
"payload": event.payload,
|
||||
});
|
||||
|
||||
let mut last_error = None;
|
||||
|
||||
for attempt in 1..=MAX_RETRIES {
|
||||
match client.post(url).json(&body).send().await {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
info!(url, attempt, "Webhook delivered successfully");
|
||||
return Ok(());
|
||||
}
|
||||
Ok(resp) => {
|
||||
let status = resp.status();
|
||||
warn!(url, attempt, %status, "Webhook returned non-success status");
|
||||
last_error = Some(format!("HTTP {status}"));
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(url, attempt, error = %e, "Webhook delivery failed");
|
||||
last_error = Some(e.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if attempt < MAX_RETRIES {
|
||||
let backoff = std::time::Duration::from_millis(500 * 2u64.pow(attempt - 1));
|
||||
tokio::time::sleep(backoff).await;
|
||||
}
|
||||
}
|
||||
|
||||
bail!(
|
||||
"Webhook delivery failed after {MAX_RETRIES} attempts: {}",
|
||||
last_error.unwrap_or_default()
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod file;
|
||||
pub mod http;
|
||||
pub mod notification;
|
||||
@@ -0,0 +1,18 @@
|
||||
use anyhow::Result;
|
||||
use tracing::info;
|
||||
|
||||
use cxcloud_common::event::Event;
|
||||
|
||||
/// Deliver event as a notification (stub — would integrate with Slack, email, etc.)
|
||||
pub async fn deliver(event: &Event) -> Result<()> {
|
||||
let channel = event
|
||||
.metadata
|
||||
.get("notification_channel")
|
||||
.map(|v| v.as_str())
|
||||
.unwrap_or("default");
|
||||
|
||||
info!(event_id = %event.id, channel, "Notification delivered");
|
||||
|
||||
// Stub: real implementation would POST to Slack webhook, send email, etc.
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
use anyhow::Result;
|
||||
use tracing::info;
|
||||
|
||||
use cxcloud_common::{event::Event, redis_streams};
|
||||
|
||||
/// Send a feedback event to events.feedback stream.
|
||||
pub async fn send_feedback(
|
||||
redis: &redis::aio::ConnectionManager,
|
||||
original_event: &Event,
|
||||
success: bool,
|
||||
error_message: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let feedback = Event::new(
|
||||
"output-service",
|
||||
"delivery_feedback",
|
||||
serde_json::json!({
|
||||
"original_event_id": original_event.id,
|
||||
"original_event_type": original_event.r#type,
|
||||
"delivery_success": success,
|
||||
"error_message": error_message,
|
||||
}),
|
||||
);
|
||||
|
||||
redis_streams::xadd(redis, "events.feedback", &feedback.to_stream_fields()).await?;
|
||||
info!(
|
||||
original_event_id = %original_event.id,
|
||||
success,
|
||||
"Feedback sent"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
mod consumer;
|
||||
mod delivery;
|
||||
mod feedback;
|
||||
mod router;
|
||||
|
||||
use axum::{
|
||||
extract::State,
|
||||
response::Json,
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tracing::info;
|
||||
|
||||
use cxcloud_common::{
|
||||
config::{env_or, env_port},
|
||||
health::HealthResponse,
|
||||
redis_streams,
|
||||
telemetry,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub redis: redis::aio::ConnectionManager,
|
||||
pub http_client: reqwest::Client,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let otel_endpoint = env_or("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317");
|
||||
let log_level = env_or("LOG_LEVEL", "info");
|
||||
telemetry::init("output-service", &otel_endpoint, &log_level);
|
||||
|
||||
let redis_url = env_or("REDIS_URL", "redis://127.0.0.1:6379");
|
||||
let redis = redis_streams::connect(&redis_url).await?;
|
||||
|
||||
// Ensure consumer group
|
||||
redis_streams::ensure_consumer_group(&redis, "events.output", "output-group").await;
|
||||
|
||||
let http_client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.build()?;
|
||||
|
||||
let state = Arc::new(AppState {
|
||||
redis: redis.clone(),
|
||||
http_client,
|
||||
});
|
||||
|
||||
// Spawn the Redis stream consumer
|
||||
let consumer_state = state.clone();
|
||||
tokio::spawn(async move {
|
||||
consumer::run(consumer_state).await;
|
||||
});
|
||||
|
||||
let app = Router::new()
|
||||
.route("/health", get(health))
|
||||
.route("/ready", get(ready))
|
||||
.route("/metrics", get(metrics))
|
||||
.with_state(state);
|
||||
|
||||
let port = env_port("OUTPUT_HTTP_PORT", 8003);
|
||||
info!(port, "Output service starting");
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{port}")).await?;
|
||||
axum::serve(listener, app).await?;
|
||||
|
||||
telemetry::shutdown();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn health() -> Json<HealthResponse> {
|
||||
Json(HealthResponse::healthy("output-service"))
|
||||
}
|
||||
|
||||
async fn ready(State(state): State<Arc<AppState>>) -> axum::http::StatusCode {
|
||||
if redis_streams::ping(&state.redis).await {
|
||||
axum::http::StatusCode::OK
|
||||
} else {
|
||||
axum::http::StatusCode::SERVICE_UNAVAILABLE
|
||||
}
|
||||
}
|
||||
|
||||
async fn metrics(State(_state): State<Arc<AppState>>) -> Json<serde_json::Value> {
|
||||
Json(serde_json::json!({
|
||||
"service": "output-service",
|
||||
"deliveries": 0,
|
||||
"failures": 0,
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use anyhow::Result;
|
||||
use tracing::info;
|
||||
|
||||
use cxcloud_common::event::Event;
|
||||
|
||||
use crate::{delivery, feedback, AppState};
|
||||
|
||||
/// Route an event to the appropriate delivery handler based on metadata.
|
||||
pub async fn route_and_deliver(state: &AppState, event: &Event) -> Result<()> {
|
||||
let delivery_type = event
|
||||
.metadata
|
||||
.get("delivery_type")
|
||||
.map(|v| v.as_str())
|
||||
.unwrap_or("http");
|
||||
|
||||
info!(delivery_type, event_id = %event.id, "Routing event");
|
||||
|
||||
let result = match delivery_type {
|
||||
"http" | "webhook" => delivery::http::deliver(&state.http_client, event).await,
|
||||
"notification" => delivery::notification::deliver(event).await,
|
||||
"file" => delivery::file::deliver(event).await,
|
||||
_ => {
|
||||
tracing::warn!(delivery_type, "Unknown delivery type, defaulting to HTTP");
|
||||
delivery::http::deliver(&state.http_client, event).await
|
||||
}
|
||||
};
|
||||
|
||||
// Send feedback regardless of success
|
||||
let success = result.is_ok();
|
||||
let error_msg = result.as_ref().err().map(|e| e.to_string());
|
||||
|
||||
feedback::send_feedback(&state.redis, event, success, error_msg.as_deref()).await?;
|
||||
|
||||
result
|
||||
}
|
||||
Reference in New Issue
Block a user