feat(telemetry): integrate OpenTelemetry observability stack with health metrics
- Add OpenTelemetry SDK, OTLP exporter, Prometheus integration - Implement connection tracking with active/total/disconnection metrics - Add health endpoint with uptime and connection counts - Integrate tracing spans for socket events and engine messages - Add metrics collection for event handling duration - Update health endpoint to include live runtime state - Add graceful telemetry shutdown in main function - Implement engine session active metrics tracking - Add namespace-specific attributes to connection metrics - Introduce message edit history retrieval endpoint - Add scheduled message CRUD operations and dispatcher - Update Socket.IO event registration with observability - Refactor component update to remove dead code allowance - Add comprehensive environment variables documentation - Implement detailed development guidelines in AGENTS.md
This commit is contained in:
@@ -64,7 +64,6 @@ impl MessageService {
|
||||
}
|
||||
|
||||
/// Handle `component:update` — update a component's state (e.g., disable after interaction).
|
||||
#[allow(dead_code)]
|
||||
pub async fn update_component(
|
||||
&self,
|
||||
socket: Arc<Socket>,
|
||||
|
||||
+38
-1
@@ -296,6 +296,43 @@ impl MessageService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle `message:edit_history` — retrieve the edit history for a message.
|
||||
pub async fn get_edit_history(
|
||||
&self,
|
||||
socket: Arc<Socket>,
|
||||
data: &serde_json::Value,
|
||||
) -> ImksResult<()> {
|
||||
let user_id = self.user_id(&socket)?;
|
||||
let payload = Self::first_payload(data)?;
|
||||
let message_id: Uuid = Self::parse_field(payload, "message_id")?;
|
||||
|
||||
let message = self
|
||||
.repo
|
||||
.get(message_id)
|
||||
.await?
|
||||
.ok_or_else(|| ImksError::NotFound(format!("message {message_id}")))?;
|
||||
|
||||
let channel_id_str = message.channel_id.to_string();
|
||||
let user_id_str = user_id.to_string();
|
||||
|
||||
self.ensure_readable(&channel_id_str, &user_id_str).await?;
|
||||
|
||||
let history = self.repo.get_edit_history(message_id).await?;
|
||||
let summary = self.repo.get_edit_summary(message_id).await?;
|
||||
|
||||
let _ = socket.emit(
|
||||
"message:edit_history",
|
||||
serde_json::json!({
|
||||
"message_id": message_id.to_string(),
|
||||
"edits": history,
|
||||
"edit_count": summary.edit_count,
|
||||
"last_edited_at": summary.last_edited_at,
|
||||
"last_edited_by": summary.last_edited_by,
|
||||
}),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Permission validation helpers
|
||||
|
||||
/// Full write-access gate: resolve channel + readability + membership + SEND_MESSAGE.
|
||||
@@ -481,7 +518,7 @@ impl MessageService {
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_body_size(&self, body: &str) -> ImksResult<()> {
|
||||
pub(crate) fn validate_body_size(&self, body: &str) -> ImksResult<()> {
|
||||
if body.len() > self.max_body_size {
|
||||
return Err(ImksError::InvalidInput(format!(
|
||||
"Message body exceeds max size of {} bytes (got {})",
|
||||
|
||||
+120
-4
@@ -1,15 +1,132 @@
|
||||
//! Scheduled message dispatcher on `MessageService`.
|
||||
//! Scheduled message handler on `MessageService`.
|
||||
//!
|
||||
//! A background task that periodically scans for due scheduled messages
|
||||
//! and sends them through the normal message path.
|
||||
//! Provides:
|
||||
//! - Client-facing CRUD: schedule, cancel, list pending scheduled messages
|
||||
//! - Background dispatcher: periodically scans for due scheduled messages
|
||||
//! and sends them through the normal message path.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::repo::CreateMessageInput;
|
||||
use crate::socket::socket::Socket;
|
||||
use crate::{ImksError, ImksResult};
|
||||
|
||||
use super::message::MessageService;
|
||||
|
||||
impl MessageService {
|
||||
// ── Client-facing scheduled message CRUD ──
|
||||
|
||||
/// Handle `message:schedule` — schedule a message to be sent at a future time.
|
||||
pub async fn schedule_message(
|
||||
&self,
|
||||
socket: Arc<Socket>,
|
||||
data: &serde_json::Value,
|
||||
) -> ImksResult<()> {
|
||||
let user_id = self.user_id(&socket)?;
|
||||
let payload = Self::first_payload(data)?;
|
||||
|
||||
let channel_id: Uuid = Self::parse_field(payload, "channel_id")?;
|
||||
let body: String = Self::parse_field(payload, "body")?;
|
||||
let thread_id: Option<Uuid> = Self::parse_optional(payload, "thread_id")?;
|
||||
let reply_to_message_id: Option<Uuid> =
|
||||
Self::parse_optional(payload, "reply_to_message_id")?;
|
||||
let metadata: Option<serde_json::Value> =
|
||||
Self::parse_optional(payload, "metadata")?;
|
||||
let scheduled_at_str: String = Self::parse_field(payload, "scheduled_at")?;
|
||||
|
||||
let scheduled_at: DateTime<Utc> = chrono::DateTime::parse_from_rfc3339(&scheduled_at_str)
|
||||
.map_err(|e| ImksError::InvalidInput(format!("Invalid scheduled_at: {e}")))?
|
||||
.into();
|
||||
|
||||
let channel_id_str = channel_id.to_string();
|
||||
let user_id_str = user_id.to_string();
|
||||
|
||||
self.validate_body_size(&body)?;
|
||||
self.ensure_readable(&channel_id_str, &user_id_str).await?;
|
||||
self.ensure_member(&channel_id_str, &user_id_str).await?;
|
||||
|
||||
// Validate scheduled_at is in the future
|
||||
if scheduled_at <= Utc::now() {
|
||||
return Err(ImksError::InvalidInput(
|
||||
"scheduled_at must be in the future".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let scheduled = self
|
||||
.repo
|
||||
.schedule_message(
|
||||
channel_id,
|
||||
user_id,
|
||||
thread_id,
|
||||
reply_to_message_id,
|
||||
&body,
|
||||
metadata,
|
||||
scheduled_at,
|
||||
)
|
||||
.await?;
|
||||
|
||||
tracing::info!(
|
||||
scheduled_id = %scheduled.id,
|
||||
channel_id = %channel_id,
|
||||
user_id = %user_id,
|
||||
scheduled_at = %scheduled_at,
|
||||
"Message scheduled"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle `message:cancel_scheduled` — cancel a pending scheduled message.
|
||||
pub async fn cancel_scheduled(
|
||||
&self,
|
||||
socket: Arc<Socket>,
|
||||
data: &serde_json::Value,
|
||||
) -> ImksResult<()> {
|
||||
let user_id = self.user_id(&socket)?;
|
||||
let payload = Self::first_payload(data)?;
|
||||
let scheduled_id: Uuid = Self::parse_field(payload, "scheduled_id")?;
|
||||
|
||||
let cancelled = self.repo.cancel_scheduled(scheduled_id).await?;
|
||||
|
||||
if !cancelled {
|
||||
return Err(ImksError::NotFound(format!(
|
||||
"scheduled message {scheduled_id} not found or already processed"
|
||||
)));
|
||||
}
|
||||
|
||||
tracing::info!(%scheduled_id, %user_id, "Scheduled message cancelled");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle `message:list_scheduled` — list pending scheduled messages for a channel.
|
||||
pub async fn list_scheduled(
|
||||
&self,
|
||||
socket: Arc<Socket>,
|
||||
data: &serde_json::Value,
|
||||
) -> ImksResult<()> {
|
||||
let user_id = self.user_id(&socket)?;
|
||||
let payload = Self::first_payload(data)?;
|
||||
let channel_id: Uuid = Self::parse_field(payload, "channel_id")?;
|
||||
|
||||
let channel_id_str = channel_id.to_string();
|
||||
let user_id_str = user_id.to_string();
|
||||
|
||||
self.ensure_readable(&channel_id_str, &user_id_str).await?;
|
||||
|
||||
let scheduled = self.repo.list_scheduled(channel_id, user_id).await?;
|
||||
|
||||
let _ = socket.emit(
|
||||
"scheduled:loaded",
|
||||
serde_json::to_value(&scheduled).unwrap_or_default(),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── Background dispatcher ──
|
||||
|
||||
/// Start the background scheduled-message dispatcher.
|
||||
/// Scans every 30 seconds for pending messages whose `scheduled_at` has passed.
|
||||
pub fn start_scheduled_dispatcher(self: std::sync::Arc<Self>) {
|
||||
@@ -55,7 +172,6 @@ impl MessageService {
|
||||
.mark_scheduled_sent(scheduled.id, message.id)
|
||||
.await?;
|
||||
|
||||
// Broadcast to channel
|
||||
if let Some(ns) = self.namespaces.get_namespace("/") {
|
||||
ns.emit_to_room(
|
||||
&scheduled.channel_id.to_string(),
|
||||
|
||||
Reference in New Issue
Block a user