vendor: update cargo-cxcloud-output-0.1.0

This commit is contained in:
cx-git-agent
2026-04-26 16:48:25 +00:00
committed by GitHub
parent f62a8aed46
commit 2a568e5a45
13 changed files with 3201 additions and 3 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"git": {
"sha1": "927a31cbf65f78c3ef6b729631b2fc35335afe06",
"dirty": true
},
"path_in_vcs": "services/cxcloud-rs/crates/output"
}
Generated
+2764
View File
File diff suppressed because it is too large Load Diff
+85
View File
@@ -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",
]
+24
View File
@@ -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 }
-3
View File
@@ -1,3 +0,0 @@
# cargo-cxcloud-output-0.1.0
Cargo crate: cxcloud-output-0.1.0
+57
View File
@@ -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");
}
}
}
}
}
}
+34
View File
@@ -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(())
}
+53
View File
@@ -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()
)
}
+3
View File
@@ -0,0 +1,3 @@
pub mod file;
pub mod http;
pub mod notification;
+18
View File
@@ -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(())
}
+32
View File
@@ -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
View File
@@ -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,
}))
}
+35
View File
@@ -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
}