feat: add server-sent events for logging and activity tracking
- Implemented a custom tracing layer (`AppLogLayer`) to capture log events and broadcast them to SSE clients. - Created admin routes for streaming server logs and listing recent activity logs. - Added an activity log repository interface and SQLite implementation for persisting activity events. - Integrated activity logging into user authentication and channel CRUD operations. - Developed frontend components for displaying server logs and activity logs in the admin panel. - Enhanced the video player with a stats overlay for monitoring streaming metrics.
This commit is contained in:
5
k-tv-backend/infra/src/activity_log_repository/mod.rs
Normal file
5
k-tv-backend/infra/src/activity_log_repository/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
#[cfg(feature = "sqlite")]
|
||||
mod sqlite;
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
pub use sqlite::SqliteActivityLogRepository;
|
||||
71
k-tv-backend/infra/src/activity_log_repository/sqlite.rs
Normal file
71
k-tv-backend/infra/src/activity_log_repository/sqlite.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use async_trait::async_trait;
|
||||
use chrono::Utc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use domain::{ActivityEvent, ActivityLogRepository, DomainError, DomainResult};
|
||||
|
||||
pub struct SqliteActivityLogRepository {
|
||||
pool: sqlx::SqlitePool,
|
||||
}
|
||||
|
||||
impl SqliteActivityLogRepository {
|
||||
pub fn new(pool: sqlx::SqlitePool) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ActivityLogRepository for SqliteActivityLogRepository {
|
||||
async fn log(
|
||||
&self,
|
||||
event_type: &str,
|
||||
detail: &str,
|
||||
channel_id: Option<Uuid>,
|
||||
) -> DomainResult<()> {
|
||||
let id = Uuid::new_v4().to_string();
|
||||
let timestamp = Utc::now().to_rfc3339();
|
||||
let channel_id_str = channel_id.map(|id| id.to_string());
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO activity_log (id, timestamp, event_type, detail, channel_id) VALUES (?, ?, ?, ?, ?)",
|
||||
)
|
||||
.bind(&id)
|
||||
.bind(×tamp)
|
||||
.bind(event_type)
|
||||
.bind(detail)
|
||||
.bind(&channel_id_str)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::RepositoryError(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn recent(&self, limit: u32) -> DomainResult<Vec<ActivityEvent>> {
|
||||
let rows: Vec<(String, String, String, String, Option<String>)> = sqlx::query_as(
|
||||
"SELECT id, timestamp, event_type, detail, channel_id FROM activity_log ORDER BY timestamp DESC LIMIT ?",
|
||||
)
|
||||
.bind(limit)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::RepositoryError(e.to_string()))?;
|
||||
|
||||
let events = rows
|
||||
.into_iter()
|
||||
.filter_map(|(id, timestamp, event_type, detail, channel_id)| {
|
||||
let id = Uuid::parse_str(&id).ok()?;
|
||||
let timestamp = timestamp.parse().ok()?;
|
||||
let channel_id = channel_id.and_then(|s| Uuid::parse_str(&s).ok());
|
||||
Some(ActivityEvent {
|
||||
id,
|
||||
timestamp,
|
||||
event_type,
|
||||
detail,
|
||||
channel_id,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::db::DatabasePool;
|
||||
use domain::{ChannelRepository, ScheduleRepository, UserRepository};
|
||||
use domain::{ActivityLogRepository, ChannelRepository, ScheduleRepository, UserRepository};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum FactoryError {
|
||||
@@ -51,6 +51,25 @@ pub async fn build_channel_repository(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn build_activity_log_repository(
|
||||
pool: &DatabasePool,
|
||||
) -> FactoryResult<Arc<dyn ActivityLogRepository>> {
|
||||
match pool {
|
||||
#[cfg(feature = "sqlite")]
|
||||
DatabasePool::Sqlite(pool) => Ok(Arc::new(
|
||||
crate::activity_log_repository::SqliteActivityLogRepository::new(pool.clone()),
|
||||
)),
|
||||
#[cfg(feature = "postgres")]
|
||||
DatabasePool::Postgres(_pool) => Err(FactoryError::NotImplemented(
|
||||
"ActivityLogRepository not yet implemented for Postgres".to_string(),
|
||||
)),
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => Err(FactoryError::NotImplemented(
|
||||
"No database feature enabled".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn build_schedule_repository(
|
||||
pool: &DatabasePool,
|
||||
) -> FactoryResult<Arc<dyn ScheduleRepository>> {
|
||||
|
||||
@@ -18,6 +18,7 @@ pub mod db;
|
||||
pub mod factory;
|
||||
pub mod jellyfin;
|
||||
pub mod provider_registry;
|
||||
mod activity_log_repository;
|
||||
mod channel_repository;
|
||||
mod schedule_repository;
|
||||
mod user_repository;
|
||||
@@ -29,6 +30,8 @@ pub mod local_files;
|
||||
pub use db::run_migrations;
|
||||
pub use provider_registry::ProviderRegistry;
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
pub use activity_log_repository::SqliteActivityLogRepository;
|
||||
#[cfg(feature = "sqlite")]
|
||||
pub use user_repository::SqliteUserRepository;
|
||||
#[cfg(feature = "sqlite")]
|
||||
|
||||
Reference in New Issue
Block a user