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:
zhenyi
2026-06-11 13:53:29 +08:00
parent 40241e5db3
commit 0dbac480ae
22 changed files with 3116 additions and 64 deletions
+120 -4
View File
@@ -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(),