diff --git a/_database/sql/migrations/2026-07-02-192433-0000_alter_debug_logs_add_log_level/down.sql b/_database/sql/migrations/2026-07-02-192433-0000_alter_debug_logs_add_log_level/down.sql new file mode 100644 index 0000000000..1c014a0158 --- /dev/null +++ b/_database/sql/migrations/2026-07-02-192433-0000_alter_debug_logs_add_log_level/down.sql @@ -0,0 +1,6 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +ALTER TABLE debug_logs + DROP COLUMN maybe_log_level; diff --git a/_database/sql/migrations/2026-07-02-192433-0000_alter_debug_logs_add_log_level/up.sql b/_database/sql/migrations/2026-07-02-192433-0000_alter_debug_logs_add_log_level/up.sql new file mode 100644 index 0000000000..2e192b14c4 --- /dev/null +++ b/_database/sql/migrations/2026-07-02-192433-0000_alter_debug_logs_add_log_level/up.sql @@ -0,0 +1,6 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +ALTER TABLE debug_logs + ADD COLUMN maybe_log_level VARCHAR(16) DEFAULT NULL AFTER debug_log_type; diff --git a/_database/sql/migrations/2026-07-02-192433-0001_alter_debug_logs_add_log_level_index/down.sql b/_database/sql/migrations/2026-07-02-192433-0001_alter_debug_logs_add_log_level_index/down.sql new file mode 100644 index 0000000000..a3d01a8713 --- /dev/null +++ b/_database/sql/migrations/2026-07-02-192433-0001_alter_debug_logs_add_log_level_index/down.sql @@ -0,0 +1,6 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +ALTER TABLE debug_logs + DROP INDEX index_maybe_log_level; diff --git a/_database/sql/migrations/2026-07-02-192433-0001_alter_debug_logs_add_log_level_index/up.sql b/_database/sql/migrations/2026-07-02-192433-0001_alter_debug_logs_add_log_level_index/up.sql new file mode 100644 index 0000000000..87afcb34c2 --- /dev/null +++ b/_database/sql/migrations/2026-07-02-192433-0001_alter_debug_logs_add_log_level_index/up.sql @@ -0,0 +1,6 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +ALTER TABLE debug_logs + ADD INDEX index_maybe_log_level (maybe_log_level); diff --git a/_database/sql/migrations_squashed/debug_logs.sql b/_database/sql/migrations_squashed/debug_logs.sql index 6c681d2fc3..90846fe3ff 100644 --- a/_database/sql/migrations_squashed/debug_logs.sql +++ b/_database/sql/migrations_squashed/debug_logs.sql @@ -13,6 +13,9 @@ CREATE TABLE debug_logs ( -- The type of debug log entry. debug_log_type VARCHAR(24) NOT NULL, + -- The severity level of the log entry (info, warn, error, debug, trace). + maybe_log_level VARCHAR(16) DEFAULT NULL, + -- The user who created the log entry (if any). maybe_creator_user_token VARCHAR(32) DEFAULT NULL, @@ -25,6 +28,7 @@ CREATE TABLE debug_logs ( PRIMARY KEY (id), KEY index_event_token (event_token), KEY index_debug_log_type (debug_log_type), + KEY index_maybe_log_level (maybe_log_level), KEY index_maybe_creator_user_token (maybe_creator_user_token), KEY index_created_at (created_at) diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/debug_logs/debug_log_entry.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/debug_logs/debug_log_entry.rs new file mode 100644 index 0000000000..24a9645ed7 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/debug_logs/debug_log_entry.rs @@ -0,0 +1,21 @@ +use chrono::{DateTime, Utc}; +use serde_derive::Serialize; +use utoipa::ToSchema; + +use enums::by_table::debug_logs::debug_log_level::DebugLogLevel; +use enums::by_table::debug_logs::debug_log_type::DebugLogType; +use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken; +use tokens::tokens::users::UserToken; + +/// A single debug log row. +/// Shared by the token-scoped and user-scoped moderation list endpoints. +#[derive(Serialize, ToSchema)] +pub struct ModerationDebugLogEntry { + pub id: u64, + pub event_token: DebugLogEventToken, + pub debug_log_type: DebugLogType, + pub maybe_log_level: Option, + pub maybe_creator_user_token: Option, + pub message: String, + pub created_at: DateTime, +} diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/debug_logs/mod.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/debug_logs/mod.rs new file mode 100644 index 0000000000..e8e6926c67 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/debug_logs/mod.rs @@ -0,0 +1,4 @@ +pub mod debug_log_entry; +pub mod moderation_list_all_debug_logs; +pub mod moderation_list_debug_logs_for_token; +pub mod moderation_list_debug_logs_for_user; diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/debug_logs/moderation_list_all_debug_logs.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/debug_logs/moderation_list_all_debug_logs.rs new file mode 100644 index 0000000000..8e7acb60f6 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/debug_logs/moderation_list_all_debug_logs.rs @@ -0,0 +1,79 @@ +use chrono::{DateTime, Utc}; +use serde_derive::{Deserialize, Serialize}; +use utoipa::{IntoParams, ToSchema}; + +use enums::by_table::debug_logs::debug_log_level::DebugLogLevel; +use enums::by_table::debug_logs::debug_log_type::DebugLogType; +use enums::error::enum_error::EnumError; +use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken; +use tokens::tokens::users::UserToken; + +pub const MODERATION_LIST_ALL_DEBUG_LOGS_PATH: &str = "/v1/moderation/debug_logs/list_all"; + +// ── Query params ── + +#[derive(Deserialize, IntoParams, ToSchema)] +pub struct ModerationListAllDebugLogsQueryParams { + /// Optional comma-separated list of severity levels to include, + /// eg. `severities=error,warn`. Omit to include all rows. + pub severities: Option, + + /// Cursor for pagination. Pass the `next_cursor` from a previous response. + pub cursor: Option, + + /// Max rows per page (default 50, max 200). + pub limit: Option, +} + +impl ModerationListAllDebugLogsQueryParams { + /// Parse the comma-separated `severities` parameter. + /// Returns `None` when the parameter is absent or empty. + pub fn parsed_severities(&self) -> Result>, EnumError> { + let raw = match self.severities.as_deref() { + Some(value) if !value.trim().is_empty() => value, + _ => return Ok(None), + }; + + let levels = raw + .split(',') + .map(str::trim) + .filter(|part| !part.is_empty()) + .map(DebugLogLevel::from_str) + .collect::, EnumError>>()?; + + Ok(if levels.is_empty() { None } else { Some(levels) }) + } +} + +// ── Response ── + +#[derive(Serialize, ToSchema)] +pub struct ModerationListAllDebugLogsSuccessResponse { + pub success: bool, + /// Most recent first. + pub debug_logs: Vec, + /// Cursor for the next page. `None` if there are no more results. + pub next_cursor: Option, +} + +/// A debug log row with the creator user joined in (when one exists). +#[derive(Serialize, ToSchema)] +pub struct ModerationListAllDebugLogsEntry { + pub id: u64, + pub event_token: DebugLogEventToken, + pub debug_log_type: DebugLogType, + pub maybe_log_level: Option, + pub maybe_creator_user_token: Option, + pub message: String, + pub created_at: DateTime, + pub maybe_user: Option, +} + +/// The creator user of a debug log row. +#[derive(Serialize, ToSchema)] +pub struct ModerationListAllDebugLogsUser { + pub user_token: UserToken, + pub display_name: String, + pub username: String, + pub gravatar_hash: String, +} diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/debug_logs/moderation_list_debug_logs_for_token.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/debug_logs/moderation_list_debug_logs_for_token.rs new file mode 100644 index 0000000000..b3b50845e9 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/debug_logs/moderation_list_debug_logs_for_token.rs @@ -0,0 +1,30 @@ +use serde_derive::{Deserialize, Serialize}; +use utoipa::{IntoParams, ToSchema}; + +use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken; + +use crate::moderation::debug_logs::debug_log_entry::ModerationDebugLogEntry; + +pub const MODERATION_LIST_DEBUG_LOGS_FOR_TOKEN_PATH: &str = "/v1/moderation/debug_logs/list/{token}"; + +// ── Path params ── + +#[derive(Deserialize, ToSchema)] +pub struct ModerationListDebugLogsForTokenPathInfo { + pub token: DebugLogEventToken, +} + +// ── Query params ── + +#[derive(Deserialize, IntoParams, ToSchema)] +pub struct ModerationListDebugLogsForTokenQueryParams { + pub limit: Option, +} + +// ── Response ── + +#[derive(Serialize, ToSchema)] +pub struct ModerationListDebugLogsForTokenSuccessResponse { + pub success: bool, + pub debug_logs: Vec, +} diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/debug_logs/moderation_list_debug_logs_for_user.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/debug_logs/moderation_list_debug_logs_for_user.rs new file mode 100644 index 0000000000..cc700e324a --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/debug_logs/moderation_list_debug_logs_for_user.rs @@ -0,0 +1,37 @@ +use serde_derive::{Deserialize, Serialize}; +use utoipa::{IntoParams, ToSchema}; + +use tokens::tokens::users::UserToken; + +use crate::moderation::debug_logs::debug_log_entry::ModerationDebugLogEntry; + +pub const MODERATION_LIST_DEBUG_LOGS_FOR_USER_PATH: &str = "/v1/moderation/debug_logs/user_list/{user_token}"; + +// ── Path params ── + +#[derive(Deserialize, ToSchema)] +pub struct ModerationListDebugLogsForUserPathInfo { + pub user_token: UserToken, +} + +// ── Query params ── + +#[derive(Deserialize, IntoParams, ToSchema)] +pub struct ModerationListDebugLogsForUserQueryParams { + /// Cursor for pagination. Pass the `next_cursor` from a previous response. + pub cursor: Option, + + /// Max rows per page (default 50, max 200). + pub limit: Option, +} + +// ── Response ── + +#[derive(Serialize, ToSchema)] +pub struct ModerationListDebugLogsForUserSuccessResponse { + pub success: bool, + /// Most recent first. + pub debug_logs: Vec, + /// Cursor for the next page. `None` if there are no more results. + pub next_cursor: Option, +} diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/mod.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/mod.rs index ea1932fddf..c5b7565be2 100644 --- a/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/mod.rs +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/mod.rs @@ -1,4 +1,5 @@ pub mod alerts; +pub mod debug_logs; pub mod jobs; pub mod user; pub mod user_referrals; diff --git a/crates/schema/database/mysql_queries/src/queries/debug_logs/bulk_insert_debug_logs.rs b/crates/schema/database/mysql_queries/src/queries/debug_logs/bulk_insert_debug_logs.rs index b0ea96c899..6c6a1a4329 100644 --- a/crates/schema/database/mysql_queries/src/queries/debug_logs/bulk_insert_debug_logs.rs +++ b/crates/schema/database/mysql_queries/src/queries/debug_logs/bulk_insert_debug_logs.rs @@ -3,6 +3,7 @@ use std::marker::PhantomData; use log::warn; use sqlx::{Executor, MySql, QueryBuilder}; +use enums::by_table::debug_logs::debug_log_level::DebugLogLevel; use enums::by_table::debug_logs::debug_log_type::DebugLogType; use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken; use tokens::tokens::users::UserToken; @@ -31,6 +32,7 @@ where /// Callers should use `serde_json::to_string(...)` before constructing this. pub struct DebugLogEvent { pub debug_log_type: DebugLogType, + pub maybe_log_level: Option, pub message: String, } @@ -56,7 +58,7 @@ where let maybe_user_token_str = args.maybe_creator_user_token.map(|t| t.as_str().to_string()); let mut query_builder = QueryBuilder::new( - "INSERT INTO debug_logs (event_token, debug_log_type, maybe_creator_user_token, message) VALUES " + "INSERT INTO debug_logs (event_token, debug_log_type, maybe_log_level, maybe_creator_user_token, message) VALUES " ); for (i, event) in args.events.iter().enumerate() { @@ -65,6 +67,8 @@ where query_builder.push(", "); query_builder.push_bind(event.debug_log_type.to_str()); query_builder.push(", "); + query_builder.push_bind(event.maybe_log_level.map(|l| l.to_str())); + query_builder.push(", "); query_builder.push_bind(&maybe_user_token_str); query_builder.push(", "); query_builder.push_bind(&event.message); diff --git a/crates/schema/database/mysql_queries/src/queries/debug_logs/insert_debug_log.rs b/crates/schema/database/mysql_queries/src/queries/debug_logs/insert_debug_log.rs index 82dc58cbe5..0265dd7761 100644 --- a/crates/schema/database/mysql_queries/src/queries/debug_logs/insert_debug_log.rs +++ b/crates/schema/database/mysql_queries/src/queries/debug_logs/insert_debug_log.rs @@ -2,6 +2,7 @@ use std::marker::PhantomData; use sqlx::{Executor, MySql}; +use enums::by_table::debug_logs::debug_log_level::DebugLogLevel; use enums::by_table::debug_logs::debug_log_type::DebugLogType; use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken; use tokens::tokens::users::UserToken; @@ -20,6 +21,9 @@ where /// The type of debug log event. pub debug_log_type: DebugLogType, + /// The severity level of the log event (if any). + pub maybe_log_level: Option, + /// The log message body (pre-serialized JSON or any text). pub message: &'e str, @@ -47,11 +51,13 @@ INSERT INTO debug_logs SET event_token = ?, debug_log_type = ?, + maybe_log_level = ?, maybe_creator_user_token = ?, message = ? "#, event_token.as_str(), args.debug_log_type.to_str(), + args.maybe_log_level.map(|l| l.to_str()), args.maybe_creator_user_token.map(|t| t.as_str()), args.message, ) diff --git a/crates/schema/database/mysql_queries/src/queries/debug_logs/list_all_debug_logs.rs b/crates/schema/database/mysql_queries/src/queries/debug_logs/list_all_debug_logs.rs new file mode 100644 index 0000000000..5c74d4f0eb --- /dev/null +++ b/crates/schema/database/mysql_queries/src/queries/debug_logs/list_all_debug_logs.rs @@ -0,0 +1,142 @@ +use std::marker::PhantomData; + +use chrono::{DateTime, Utc}; +use sqlx::mysql::MySqlRow; +use sqlx::{Executor, MySql, QueryBuilder, Row}; + +use enums::by_table::debug_logs::debug_log_level::DebugLogLevel; +use enums::by_table::debug_logs::debug_log_type::DebugLogType; +use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken; +use tokens::tokens::users::UserToken; + +const DEFAULT_LIMIT: u32 = 50; +const MAX_LIMIT: u32 = 200; + +pub struct ListAllDebugLogsArgs<'c, E> +where + E: Executor<'c, Database = MySql>, +{ + /// If provided, only rows with one of these log levels are returned. + /// (Rows with a NULL log level are excluded when filtering.) + pub maybe_log_levels: Option>, + + /// Cursor for pagination: only rows with `id` strictly below this are + /// returned. Pass the `next_cursor` from a previous page. + pub maybe_id_cursor: Option, + + pub limit: Option, + pub mysql_executor: E, + pub phantom: PhantomData<&'c E>, +} + +pub struct ListAllDebugLogsResult { + pub debug_logs: Vec, + /// Cursor for the next page. `None` if there are no more results. + pub next_cursor: Option, +} + +/// A debug log row left-outer-joined against the creator user (if any). +#[derive(Debug)] +pub struct AllDebugLogRow { + pub id: u64, + pub event_token: DebugLogEventToken, + pub debug_log_type: DebugLogType, + pub maybe_log_level: Option, + pub maybe_creator_user_token: Option, + pub message: String, + pub created_at: DateTime, + + // Joined user fields (present when the creator user exists). + pub maybe_user_display_name: Option, + pub maybe_user_username: Option, + pub maybe_user_gravatar_hash: Option, +} + +/// List all debug logs, most recent first, cursor-paginated by `id`, +/// optionally scoped to a set of log levels. +/// +/// NB: Uses `QueryBuilder` (not the compile-time macros) because of the +/// dynamic `WHERE maybe_log_level IN (...)` clause. +pub async fn list_all_debug_logs<'c, E>( + args: ListAllDebugLogsArgs<'c, E>, +) -> Result +where + E: Executor<'c, Database = MySql>, +{ + let limit = args.limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT); + // Fetch limit + 1 so we can detect whether there's a next page. + let fetch_limit = (limit + 1) as i64; + + let id_cursor = args.maybe_id_cursor.unwrap_or(u64::MAX); + + let mut query_builder: QueryBuilder = QueryBuilder::new( + r#" +SELECT + d.id, + d.event_token, + d.debug_log_type, + d.maybe_log_level, + d.maybe_creator_user_token, + d.message, + d.created_at, + u.display_name as maybe_user_display_name, + u.username as maybe_user_username, + u.email_gravatar_hash as maybe_user_gravatar_hash +FROM debug_logs d +LEFT OUTER JOIN users u + ON u.token = d.maybe_creator_user_token +WHERE d.id < "#, + ); + query_builder.push_bind(id_cursor); + + if let Some(levels) = args.maybe_log_levels.as_ref().filter(|l| !l.is_empty()) { + query_builder.push(" AND d.maybe_log_level IN ("); + let mut separated = query_builder.separated(", "); + for level in levels { + separated.push_bind(level.to_str()); + } + separated.push_unseparated(")"); + } + + query_builder.push(" ORDER BY d.id DESC LIMIT "); + query_builder.push_bind(fetch_limit); + + let raw_rows: Vec = query_builder + .build() + .fetch_all(args.mysql_executor) + .await?; + + let mut debug_logs = raw_rows + .into_iter() + .map(row_from_mysql_row) + .collect::, sqlx::Error>>()?; + + let next_cursor = if debug_logs.len() > limit as usize { + debug_logs.truncate(limit as usize); + debug_logs.last().map(|row| row.id) + } else { + None + }; + + Ok(ListAllDebugLogsResult { + debug_logs, + next_cursor, + }) +} + +fn row_from_mysql_row(row: MySqlRow) -> Result { + let created_at: DateTime = row.try_get("created_at")?; + + Ok(AllDebugLogRow { + id: row.try_get("id")?, + event_token: row.try_get("event_token")?, + debug_log_type: row.try_get("debug_log_type")?, + maybe_log_level: row.try_get("maybe_log_level")?, + maybe_creator_user_token: row.try_get("maybe_creator_user_token")?, + message: row.try_get("message")?, + created_at, + maybe_user_display_name: row.try_get("maybe_user_display_name")?, + maybe_user_username: row.try_get("maybe_user_username")?, + maybe_user_gravatar_hash: row.try_get("maybe_user_gravatar_hash")?, + }) +} diff --git a/crates/schema/database/mysql_queries/src/queries/debug_logs/list_debug_logs_for_token.rs b/crates/schema/database/mysql_queries/src/queries/debug_logs/list_debug_logs_for_token.rs index e561ea4cfc..03e2a8221c 100644 --- a/crates/schema/database/mysql_queries/src/queries/debug_logs/list_debug_logs_for_token.rs +++ b/crates/schema/database/mysql_queries/src/queries/debug_logs/list_debug_logs_for_token.rs @@ -3,6 +3,7 @@ use std::marker::PhantomData; use chrono::{DateTime, NaiveDateTime, Utc}; use sqlx::{Executor, MySql}; +use enums::by_table::debug_logs::debug_log_level::DebugLogLevel; use enums::by_table::debug_logs::debug_log_type::DebugLogType; use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken; use tokens::tokens::users::UserToken; @@ -24,6 +25,7 @@ pub struct DebugLogRow { pub id: u64, pub event_token: DebugLogEventToken, pub debug_log_type: DebugLogType, + pub maybe_log_level: Option, pub maybe_creator_user_token: Option, pub message: String, pub created_at: DateTime, @@ -34,6 +36,7 @@ struct RawDebugLogRow { id: u64, event_token: DebugLogEventToken, debug_log_type: DebugLogType, + maybe_log_level: Option, maybe_creator_user_token: Option, message: String, created_at: NaiveDateTime, @@ -54,6 +57,7 @@ SELECT id as `id: u64`, event_token as `event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`, debug_log_type as `debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType`, + maybe_log_level as `maybe_log_level: enums::by_table::debug_logs::debug_log_level::DebugLogLevel`, maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`, message, created_at @@ -73,6 +77,7 @@ LIMIT ? id: row.id, event_token: row.event_token, debug_log_type: row.debug_log_type, + maybe_log_level: row.maybe_log_level, maybe_creator_user_token: row.maybe_creator_user_token, message: row.message, created_at: row.created_at.and_utc(), diff --git a/crates/schema/database/mysql_queries/src/queries/debug_logs/list_debug_logs_for_user.rs b/crates/schema/database/mysql_queries/src/queries/debug_logs/list_debug_logs_for_user.rs new file mode 100644 index 0000000000..0dd0c100d8 --- /dev/null +++ b/crates/schema/database/mysql_queries/src/queries/debug_logs/list_debug_logs_for_user.rs @@ -0,0 +1,118 @@ +use std::marker::PhantomData; + +use chrono::{DateTime, NaiveDateTime, Utc}; +use sqlx::{Executor, MySql}; + +use enums::by_table::debug_logs::debug_log_level::DebugLogLevel; +use enums::by_table::debug_logs::debug_log_type::DebugLogType; +use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken; +use tokens::tokens::users::UserToken; + +const DEFAULT_LIMIT: u32 = 50; +const MAX_LIMIT: u32 = 200; + +pub struct ListDebugLogsForUserArgs<'e, 'c, E> +where + E: 'e + Executor<'c, Database = MySql>, +{ + pub user_token: &'e UserToken, + + /// Cursor for pagination: only rows with `id` strictly below this are + /// returned. Pass the `next_cursor` from a previous page. + pub maybe_id_cursor: Option, + + pub limit: Option, + pub mysql_executor: E, + pub phantom: PhantomData<&'c E>, +} + +pub struct ListDebugLogsForUserResult { + pub debug_logs: Vec, + /// Cursor for the next page. `None` if there are no more results. + pub next_cursor: Option, +} + +#[derive(Debug)] +pub struct UserDebugLogRow { + pub id: u64, + pub event_token: DebugLogEventToken, + pub debug_log_type: DebugLogType, + pub maybe_log_level: Option, + pub maybe_creator_user_token: Option, + pub message: String, + pub created_at: DateTime, +} + +#[derive(Debug)] +struct RawUserDebugLogRow { + id: u64, + event_token: DebugLogEventToken, + debug_log_type: DebugLogType, + maybe_log_level: Option, + maybe_creator_user_token: Option, + message: String, + created_at: NaiveDateTime, +} + +/// List debug logs for a user, most recent first, cursor-paginated by `id`. +pub async fn list_debug_logs_for_user<'e, 'c: 'e, E>( + args: ListDebugLogsForUserArgs<'e, 'c, E>, +) -> Result +where + E: 'e + Executor<'c, Database = MySql>, +{ + let limit = args.limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT); + // Fetch limit + 1 so we can detect whether there's a next page. + let fetch_limit = (limit + 1) as i64; + + // Use u64::MAX when no cursor so all rows are included. + let id_cursor = args.maybe_id_cursor.unwrap_or(u64::MAX); + + let rows = sqlx::query_as!( + RawUserDebugLogRow, + r#" +SELECT + id as `id: u64`, + event_token as `event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`, + debug_log_type as `debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType`, + maybe_log_level as `maybe_log_level: enums::by_table::debug_logs::debug_log_level::DebugLogLevel`, + maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`, + message, + created_at +FROM debug_logs +WHERE maybe_creator_user_token = ? + AND id < ? +ORDER BY id DESC +LIMIT ? + "#, + args.user_token.as_str(), + id_cursor, + fetch_limit, + ) + .fetch_all(args.mysql_executor) + .await?; + + let mut debug_logs: Vec = rows.into_iter().map(|row| { + UserDebugLogRow { + id: row.id, + event_token: row.event_token, + debug_log_type: row.debug_log_type, + maybe_log_level: row.maybe_log_level, + maybe_creator_user_token: row.maybe_creator_user_token, + message: row.message, + created_at: row.created_at.and_utc(), + } + }).collect(); + + let next_cursor = if debug_logs.len() > limit as usize { + debug_logs.truncate(limit as usize); + debug_logs.last().map(|row| row.id) + } else { + None + }; + + Ok(ListDebugLogsForUserResult { + debug_logs, + next_cursor, + }) +} diff --git a/crates/schema/database/mysql_queries/src/queries/debug_logs/mod.rs b/crates/schema/database/mysql_queries/src/queries/debug_logs/mod.rs index 5554168d12..c630b7744a 100644 --- a/crates/schema/database/mysql_queries/src/queries/debug_logs/mod.rs +++ b/crates/schema/database/mysql_queries/src/queries/debug_logs/mod.rs @@ -1,3 +1,5 @@ pub mod bulk_insert_debug_logs; pub mod insert_debug_log; +pub mod list_all_debug_logs; pub mod list_debug_logs_for_token; +pub mod list_debug_logs_for_user; diff --git a/crates/schema/public/enums/src/by_table/debug_logs/debug_log_level.rs b/crates/schema/public/enums/src/by_table/debug_logs/debug_log_level.rs new file mode 100644 index 0000000000..5e7ddc1609 --- /dev/null +++ b/crates/schema/public/enums/src/by_table/debug_logs/debug_log_level.rs @@ -0,0 +1,151 @@ +use std::collections::BTreeSet; + +#[cfg(test)] +use strum::EnumCount; +#[cfg(test)] +use strum::EnumIter; +use utoipa::ToSchema; + +use crate::error::enum_error::EnumError; + +/// Maximum serialized string length for database storage. +pub const MAX_LENGTH: usize = 16; + +/// Used in the `debug_logs` table in a `VARCHAR(16)` field (`maybe_log_level`). +/// +/// DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY. +#[cfg_attr(test, derive(EnumIter, EnumCount))] +#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum DebugLogLevel { + Info, + Warn, + Error, + Debug, + Trace, +} + +impl_enum_display_and_debug_using_to_str!(DebugLogLevel); +impl_mysql_enum_coders!(DebugLogLevel); +impl_mysql_from_row!(DebugLogLevel); + +impl DebugLogLevel { + pub fn to_str(&self) -> &'static str { + match self { + Self::Info => "info", + Self::Warn => "warn", + Self::Error => "error", + Self::Debug => "debug", + Self::Trace => "trace", + } + } + + pub fn from_str(value: &str) -> Result { + match value { + "info" => Ok(Self::Info), + "warn" => Ok(Self::Warn), + "error" => Ok(Self::Error), + "debug" => Ok(Self::Debug), + "trace" => Ok(Self::Trace), + _ => Err(EnumError::CouldNotConvertFromString(value.to_string())), + } + } + + pub fn all_variants() -> BTreeSet { + BTreeSet::from([ + Self::Info, + Self::Warn, + Self::Error, + Self::Debug, + Self::Trace, + ]) + } +} + +#[cfg(test)] +mod tests { + use crate::by_table::debug_logs::debug_log_level::DebugLogLevel; + use crate::by_table::debug_logs::debug_log_level::MAX_LENGTH; + use crate::test_helpers::assert_serialization; + + mod explicit_checks { + use super::*; + use crate::error::enum_error::EnumError; + + #[test] + fn test_serialization() { + assert_serialization(DebugLogLevel::Info, "info"); + assert_serialization(DebugLogLevel::Warn, "warn"); + assert_serialization(DebugLogLevel::Error, "error"); + assert_serialization(DebugLogLevel::Debug, "debug"); + assert_serialization(DebugLogLevel::Trace, "trace"); + } + + #[test] + fn to_str() { + assert_eq!(DebugLogLevel::Info.to_str(), "info"); + assert_eq!(DebugLogLevel::Warn.to_str(), "warn"); + assert_eq!(DebugLogLevel::Error.to_str(), "error"); + assert_eq!(DebugLogLevel::Debug.to_str(), "debug"); + assert_eq!(DebugLogLevel::Trace.to_str(), "trace"); + } + + #[test] + fn from_str() { + assert_eq!(DebugLogLevel::from_str("info").unwrap(), DebugLogLevel::Info); + assert_eq!(DebugLogLevel::from_str("warn").unwrap(), DebugLogLevel::Warn); + assert_eq!(DebugLogLevel::from_str("error").unwrap(), DebugLogLevel::Error); + assert_eq!(DebugLogLevel::from_str("debug").unwrap(), DebugLogLevel::Debug); + assert_eq!(DebugLogLevel::from_str("trace").unwrap(), DebugLogLevel::Trace); + } + + #[test] + fn from_str_err() { + let result = DebugLogLevel::from_str("invalid"); + assert!(result.is_err()); + if let Err(EnumError::CouldNotConvertFromString(value)) = result { + assert_eq!(value, "invalid"); + } else { + panic!("Expected EnumError::CouldNotConvertFromString"); + } + } + + #[test] + fn all_variants() { + let mut variants = DebugLogLevel::all_variants(); + assert_eq!(variants.len(), 5); + assert_eq!(variants.pop_first(), Some(DebugLogLevel::Info)); + assert_eq!(variants.pop_first(), Some(DebugLogLevel::Warn)); + assert_eq!(variants.pop_first(), Some(DebugLogLevel::Error)); + assert_eq!(variants.pop_first(), Some(DebugLogLevel::Debug)); + assert_eq!(variants.pop_first(), Some(DebugLogLevel::Trace)); + assert_eq!(variants.pop_first(), None); + } + } + + mod mechanical_checks { + use super::*; + + #[test] + fn variant_length() { + use strum::IntoEnumIterator; + assert_eq!(DebugLogLevel::all_variants().len(), DebugLogLevel::iter().len()); + } + + #[test] + fn round_trip() { + for variant in DebugLogLevel::all_variants() { + assert_eq!(variant, DebugLogLevel::from_str(variant.to_str()).unwrap()); + assert_eq!(variant, DebugLogLevel::from_str(&format!("{}", variant)).unwrap()); + assert_eq!(variant, DebugLogLevel::from_str(&format!("{:?}", variant)).unwrap()); + } + } + + #[test] + fn max_length() { + for variant in DebugLogLevel::all_variants() { + assert!(variant.to_str().len() <= MAX_LENGTH); + } + } + } +} diff --git a/crates/schema/public/enums/src/by_table/debug_logs/mod.rs b/crates/schema/public/enums/src/by_table/debug_logs/mod.rs index 9d2e0d44fc..f67cbb4a6c 100644 --- a/crates/schema/public/enums/src/by_table/debug_logs/mod.rs +++ b/crates/schema/public/enums/src/by_table/debug_logs/mod.rs @@ -1 +1,2 @@ +pub mod debug_log_level; pub mod debug_log_type; diff --git a/crates/service/web/storyteller_web/src/docs/api_doc.rs b/crates/service/web/storyteller_web/src/docs/api_doc.rs index 8c27cb0419..c9ddb6077b 100644 --- a/crates/service/web/storyteller_web/src/docs/api_doc.rs +++ b/crates/service/web/storyteller_web/src/docs/api_doc.rs @@ -88,7 +88,10 @@ use crate::http_server::endpoints::media_files::upsert_upload::write_error::Medi use crate::http_server::endpoints::media_files::upsert_upload::write_scene_file::write_scene_file_media_file_handler::*; use crate::http_server::endpoints::model_download::enqueue_gptsovits_model_download_handler::*; use artcraft_api_defs::generate::video::edit::beeble_switchx_edit_video::*; -use crate::http_server::endpoints::moderation::debug_logs::moderation_list_debug_logs_for_token_handler::*; +use artcraft_api_defs::moderation::debug_logs::debug_log_entry::*; +use artcraft_api_defs::moderation::debug_logs::moderation_list_all_debug_logs::*; +use artcraft_api_defs::moderation::debug_logs::moderation_list_debug_logs_for_token::*; +use artcraft_api_defs::moderation::debug_logs::moderation_list_debug_logs_for_user::*; use crate::http_server::endpoints::moderation::jobs::moderation_get_job_by_token_handler::*; use crate::http_server::endpoints::moderation::user_feature_flags::moderator_edit_user_feature_flags_handler::*; use crate::http_server::endpoints::moderation::user_feature_flags::moderator_list_all_available_user_feature_flags_handler::*; @@ -602,7 +605,9 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux crate::http_server::endpoints::moderation::wallets::moderator_add_banked_balance_to_wallet_handler::moderator_add_banked_balance_to_wallet_handler, crate::http_server::endpoints::moderation::wallets::moderator_create_wallet_for_user_handler::moderator_create_wallet_for_user_handler, crate::http_server::endpoints::moderation::wallets::moderator_get_wallet_handler::moderator_get_wallet_handler, + crate::http_server::endpoints::moderation::debug_logs::moderation_list_all_debug_logs_handler::moderation_list_all_debug_logs_handler, crate::http_server::endpoints::moderation::debug_logs::moderation_list_debug_logs_for_token_handler::moderation_list_debug_logs_for_token_handler, + crate::http_server::endpoints::moderation::debug_logs::moderation_list_debug_logs_for_user_handler::moderation_list_debug_logs_for_user_handler, crate::http_server::endpoints::moderation::jobs::moderation_get_job_by_token_handler::moderation_get_job_by_token_handler, crate::http_server::endpoints::moderation::staff_audit_logs::moderator_list_staff_audit_logs_handler::moderator_list_staff_audit_logs_handler, crate::http_server::endpoints::moderation::user_referrals::moderator_list_global_user_referrals_handler::moderator_list_global_user_referrals_handler, @@ -1030,10 +1035,17 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux GetJobByTokenPathInfo, GetJobByTokenSuccessResponse, ModerationJobResponse, - ListDebugLogsPathInfo, - ListDebugLogsQueryParams, - ListDebugLogsSuccessResponse, - DebugLogEntry, + ModerationDebugLogEntry, + ModerationListAllDebugLogsEntry, + ModerationListAllDebugLogsQueryParams, + ModerationListAllDebugLogsSuccessResponse, + ModerationListAllDebugLogsUser, + ModerationListDebugLogsForTokenPathInfo, + ModerationListDebugLogsForTokenQueryParams, + ModerationListDebugLogsForTokenSuccessResponse, + ModerationListDebugLogsForUserPathInfo, + ModerationListDebugLogsForUserQueryParams, + ModerationListDebugLogsForUserSuccessResponse, ListStaffAuditLogsQueryParams, ListStaffAuditLogsSuccessResponse, ListImpersonationRequestsQueryParams, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/debug_logs/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/debug_logs/mod.rs index d4523970f7..e4fbbb0b85 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/debug_logs/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/debug_logs/mod.rs @@ -1 +1,3 @@ +pub mod moderation_list_all_debug_logs_handler; pub mod moderation_list_debug_logs_for_token_handler; +pub mod moderation_list_debug_logs_for_user_handler; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/debug_logs/moderation_list_all_debug_logs_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/debug_logs/moderation_list_all_debug_logs_handler.rs new file mode 100644 index 0000000000..d2a9ddbcd4 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/debug_logs/moderation_list_all_debug_logs_handler.rs @@ -0,0 +1,88 @@ +use std::sync::Arc; + +use actix_web::web::Json; +use actix_web::{web, HttpRequest}; +use log::warn; + +use artcraft_api_defs::moderation::debug_logs::moderation_list_all_debug_logs::{ + ModerationListAllDebugLogsEntry, ModerationListAllDebugLogsQueryParams, + ModerationListAllDebugLogsSuccessResponse, ModerationListAllDebugLogsUser, +}; +use mysql_queries::queries::debug_logs::list_all_debug_logs::{ + list_all_debug_logs, ListAllDebugLogsArgs, +}; + +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::user_lookup::user_session::require_moderator::require_moderator; +use crate::state::server_state::ServerState; + +#[utoipa::path( + get, + tag = "Moderation", + path = "/v1/moderation/debug_logs/list_all", + params( + ModerationListAllDebugLogsQueryParams, + ), + responses( + (status = 200, description = "Success", body = ModerationListAllDebugLogsSuccessResponse), + (status = 400, description = "Bad request (invalid severity)"), + (status = 401, description = "Unauthorized"), + (status = 500, description = "Server error"), + ), +)] +pub async fn moderation_list_all_debug_logs_handler( + http_request: HttpRequest, + query: web::Query, + server_state: web::Data>, +) -> Result, CommonWebError> { + let maybe_log_levels = query.parsed_severities().map_err(|err| { + CommonWebError::BadInputWithSimpleMessage(format!("invalid severities parameter: {}", err)) + })?; + + let mut mysql_connection = server_state.mysql_pool.acquire().await?; + + let _user_session = require_moderator(&http_request, &server_state.session_checker, &mut *mysql_connection).await?; + + let result = list_all_debug_logs(ListAllDebugLogsArgs { + maybe_log_levels, + maybe_id_cursor: query.cursor, + limit: query.limit, + mysql_executor: &mut *mysql_connection, + phantom: Default::default(), + }).await.map_err(|err| { + warn!("Error listing all debug logs: {:?}", err); + CommonWebError::from_error(err) + })?; + + let debug_logs: Vec = result.debug_logs.into_iter().map(|row| { + // The join only yields user fields when the creator user exists. + let maybe_user = match (row.maybe_creator_user_token.clone(), row.maybe_user_display_name, row.maybe_user_username, row.maybe_user_gravatar_hash) { + (Some(user_token), Some(display_name), Some(username), Some(gravatar_hash)) => { + Some(ModerationListAllDebugLogsUser { + user_token, + display_name, + username, + gravatar_hash, + }) + } + _ => None, + }; + + ModerationListAllDebugLogsEntry { + id: row.id, + event_token: row.event_token, + debug_log_type: row.debug_log_type, + maybe_log_level: row.maybe_log_level, + maybe_creator_user_token: row.maybe_creator_user_token, + message: row.message, + created_at: row.created_at, + maybe_user, + } + }).collect(); + + Ok(Json(ModerationListAllDebugLogsSuccessResponse { + success: true, + debug_logs, + next_cursor: result.next_cursor, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/debug_logs/moderation_list_debug_logs_for_token_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/debug_logs/moderation_list_debug_logs_for_token_handler.rs index eb2402049f..c968af1e2f 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/debug_logs/moderation_list_debug_logs_for_token_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/debug_logs/moderation_list_debug_logs_for_token_handler.rs @@ -2,80 +2,45 @@ use std::sync::Arc; use actix_web::web::Json; use actix_web::{web, HttpRequest}; -use chrono::{DateTime, Utc}; use log::warn; -use serde_derive::{Deserialize, Serialize}; -use utoipa::{IntoParams, ToSchema}; -use enums::by_table::debug_logs::debug_log_type::DebugLogType; +use artcraft_api_defs::moderation::debug_logs::debug_log_entry::ModerationDebugLogEntry; +use artcraft_api_defs::moderation::debug_logs::moderation_list_debug_logs_for_token::{ + ModerationListDebugLogsForTokenPathInfo, ModerationListDebugLogsForTokenQueryParams, + ModerationListDebugLogsForTokenSuccessResponse, +}; use mysql_queries::queries::debug_logs::list_debug_logs_for_token::{ list_debug_logs_for_token, ListDebugLogsForTokenArgs, }; -use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken; -use tokens::tokens::users::UserToken; use crate::http_server::common_responses::common_web_error::CommonWebError; use crate::http_server::user_lookup::user_session::require_moderator::require_moderator; use crate::state::server_state::ServerState; -// ── Path params ── - -#[derive(Deserialize, ToSchema)] -pub struct ListDebugLogsPathInfo { - pub token: DebugLogEventToken, -} - -// ── Query params ── - -#[derive(Deserialize, IntoParams, ToSchema)] -pub struct ListDebugLogsQueryParams { - pub limit: Option, -} - -// ── Response ── - -#[derive(Serialize, ToSchema)] -pub struct ListDebugLogsSuccessResponse { - pub success: bool, - pub debug_logs: Vec, -} - -#[derive(Serialize, ToSchema)] -pub struct DebugLogEntry { - pub id: u64, - pub event_token: DebugLogEventToken, - pub debug_log_type: DebugLogType, - pub maybe_creator_user_token: Option, - pub message: String, - pub created_at: DateTime, -} - -// ── Handler ── - #[utoipa::path( get, tag = "Moderation", path = "/v1/moderation/debug_logs/list/{token}", params( ("token" = String, Path, description = "Debug log event token"), - ListDebugLogsQueryParams, + ModerationListDebugLogsForTokenQueryParams, ), responses( - (status = 200, description = "Success", body = ListDebugLogsSuccessResponse), + (status = 200, description = "Success", body = ModerationListDebugLogsForTokenSuccessResponse), (status = 401, description = "Unauthorized"), (status = 500, description = "Server error"), ), )] pub async fn moderation_list_debug_logs_for_token_handler( http_request: HttpRequest, - path: web::Path, - query: web::Query, + path: web::Path, + query: web::Query, server_state: web::Data>, -) -> Result, CommonWebError> { - let _user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?; - +) -> Result, CommonWebError> { let mut mysql_connection = server_state.mysql_pool.acquire().await?; + let _user_session = require_moderator(&http_request, &server_state.session_checker, &mut *mysql_connection).await?; + let rows = list_debug_logs_for_token(ListDebugLogsForTokenArgs { event_token: &path.token, limit: query.limit, @@ -86,18 +51,19 @@ pub async fn moderation_list_debug_logs_for_token_handler( CommonWebError::from_error(err) })?; - let debug_logs: Vec = rows.into_iter().map(|row| { - DebugLogEntry { + let debug_logs: Vec = rows.into_iter().map(|row| { + ModerationDebugLogEntry { id: row.id, event_token: row.event_token, debug_log_type: row.debug_log_type, + maybe_log_level: row.maybe_log_level, maybe_creator_user_token: row.maybe_creator_user_token, message: row.message, created_at: row.created_at, } }).collect(); - Ok(Json(ListDebugLogsSuccessResponse { + Ok(Json(ModerationListDebugLogsForTokenSuccessResponse { success: true, debug_logs, })) diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/debug_logs/moderation_list_debug_logs_for_user_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/debug_logs/moderation_list_debug_logs_for_user_handler.rs new file mode 100644 index 0000000000..be52e76460 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/debug_logs/moderation_list_debug_logs_for_user_handler.rs @@ -0,0 +1,72 @@ +use std::sync::Arc; + +use actix_web::web::Json; +use actix_web::{web, HttpRequest}; +use log::warn; + +use artcraft_api_defs::moderation::debug_logs::debug_log_entry::ModerationDebugLogEntry; +use artcraft_api_defs::moderation::debug_logs::moderation_list_debug_logs_for_user::{ + ModerationListDebugLogsForUserPathInfo, ModerationListDebugLogsForUserQueryParams, + ModerationListDebugLogsForUserSuccessResponse, +}; +use mysql_queries::queries::debug_logs::list_debug_logs_for_user::{ + list_debug_logs_for_user, ListDebugLogsForUserArgs, +}; + +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::user_lookup::user_session::require_moderator::require_moderator; +use crate::state::server_state::ServerState; + +#[utoipa::path( + get, + tag = "Moderation", + path = "/v1/moderation/debug_logs/user_list/{user_token}", + params( + ("user_token" = String, Path, description = "User token"), + ModerationListDebugLogsForUserQueryParams, + ), + responses( + (status = 200, description = "Success", body = ModerationListDebugLogsForUserSuccessResponse), + (status = 401, description = "Unauthorized"), + (status = 500, description = "Server error"), + ), +)] +pub async fn moderation_list_debug_logs_for_user_handler( + http_request: HttpRequest, + path: web::Path, + query: web::Query, + server_state: web::Data>, +) -> Result, CommonWebError> { + let mut mysql_connection = server_state.mysql_pool.acquire().await?; + + let _user_session = require_moderator(&http_request, &server_state.session_checker, &mut *mysql_connection).await?; + + let result = list_debug_logs_for_user(ListDebugLogsForUserArgs { + user_token: &path.user_token, + maybe_id_cursor: query.cursor, + limit: query.limit, + mysql_executor: &mut *mysql_connection, + phantom: Default::default(), + }).await.map_err(|err| { + warn!("Error listing debug logs for user {}: {:?}", path.user_token, err); + CommonWebError::from_error(err) + })?; + + let debug_logs: Vec = result.debug_logs.into_iter().map(|row| { + ModerationDebugLogEntry { + id: row.id, + event_token: row.event_token, + debug_log_type: row.debug_log_type, + maybe_log_level: row.maybe_log_level, + maybe_creator_user_token: row.maybe_creator_user_token, + message: row.message, + created_at: row.created_at, + } + }).collect(); + + Ok(Json(ModerationListDebugLogsForUserSuccessResponse { + success: true, + debug_logs, + next_cursor: result.next_cursor, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_api/generate/image/omni_api_image_generate_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_api/generate/image/omni_api_image_generate_handler.rs index 5ae8c48e43..28f6323fc6 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_api/generate/image/omni_api_image_generate_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_api/generate/image/omni_api_image_generate_handler.rs @@ -138,6 +138,7 @@ pub async fn omni_api_image_generate_handler( apriori_debug_log_event_token: Some(&debug_log_event_token), maybe_creator_user_token: Some(user_token), debug_log_type: DebugLogType::HttpRequest, + maybe_log_level: None, message: &serde_json::to_string(&*request).unwrap_or_default(), mysql_executor: &mut *mysql_connection, phantom: Default::default(), @@ -169,6 +170,7 @@ pub async fn omni_api_image_generate_handler( apriori_debug_log_event_token: Some(&debug_log_event_token), maybe_creator_user_token: Some(user_token), debug_log_type: DebugLogType::FalRequest, + maybe_log_level: None, message: &format!("{:#?}", outbound_request), mysql_executor: &mut *mysql_connection, phantom: Default::default(), diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_api/generate/video/omni_api_video_generate_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_api/generate/video/omni_api_video_generate_handler.rs index 76a9792c9e..b39238a952 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_api/generate/video/omni_api_video_generate_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_api/generate/video/omni_api_video_generate_handler.rs @@ -205,6 +205,7 @@ pub async fn omni_api_video_generate_handler( apriori_debug_log_event_token: Some(&debug_log_event_token), maybe_creator_user_token: Some(user_token), debug_log_type: DebugLogType::HttpRequest, + maybe_log_level: None, message: &serde_json::to_string(&*request).unwrap_or_default(), mysql_executor: &mut *mysql_connection, phantom: Default::default(), @@ -236,6 +237,7 @@ pub async fn omni_api_video_generate_handler( apriori_debug_log_event_token: Some(&debug_log_event_token), maybe_creator_user_token: Some(user_token), debug_log_type: DebugLogType::FalRequest, + maybe_log_level: None, message: &format!("{:#?}", outbound_request), mysql_executor: &mut *mysql_connection, phantom: Default::default(), @@ -253,6 +255,7 @@ pub async fn omni_api_video_generate_handler( apriori_debug_log_event_token: Some(&debug_log_event_token), maybe_creator_user_token: Some(user_token), debug_log_type: DebugLogType::GrokApiRequest, + maybe_log_level: None, message: &format!("{:#?}", outbound_request), mysql_executor: &mut *mysql_connection, phantom: Default::default(), diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/omni_gen_image_generate_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/omni_gen_image_generate_handler.rs index 361c7250c0..1e14aed4f7 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/omni_gen_image_generate_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/omni_gen_image_generate_handler.rs @@ -134,6 +134,7 @@ pub async fn omni_gen_image_generate_handler( apriori_debug_log_event_token: Some(&debug_log_event_token), maybe_creator_user_token: Some(user_token), debug_log_type: DebugLogType::HttpRequest, + maybe_log_level: None, message: &serde_json::to_string(&*request).unwrap_or_default(), mysql_executor: &mut *mysql_connection, phantom: Default::default(), @@ -165,6 +166,7 @@ pub async fn omni_gen_image_generate_handler( apriori_debug_log_event_token: Some(&debug_log_event_token), maybe_creator_user_token: Some(user_token), debug_log_type: DebugLogType::FalRequest, + maybe_log_level: None, message: &format!("{:#?}", outbound_request), mysql_executor: &mut *mysql_connection, phantom: Default::default(), diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/omni_gen_video_generate_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/omni_gen_video_generate_handler.rs index d1895e7c38..a9ac06ee45 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/omni_gen_video_generate_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/omni_gen_video_generate_handler.rs @@ -195,6 +195,7 @@ pub async fn omni_gen_video_generate_handler( apriori_debug_log_event_token: Some(&debug_log_event_token), maybe_creator_user_token: Some(user_token), debug_log_type: DebugLogType::HttpRequest, + maybe_log_level: None, message: &serde_json::to_string(&*request).unwrap_or_default(), mysql_executor: &mut *mysql_connection, phantom: Default::default(), @@ -226,6 +227,7 @@ pub async fn omni_gen_video_generate_handler( apriori_debug_log_event_token: Some(&debug_log_event_token), maybe_creator_user_token: Some(user_token), debug_log_type: DebugLogType::FalRequest, + maybe_log_level: None, message: &format!("{:#?}", outbound_request), mysql_executor: &mut *mysql_connection, phantom: Default::default(), @@ -243,6 +245,7 @@ pub async fn omni_gen_video_generate_handler( apriori_debug_log_event_token: Some(&debug_log_event_token), maybe_creator_user_token: Some(user_token), debug_log_type: DebugLogType::GrokApiRequest, + maybe_log_level: None, message: &format!("{:#?}", outbound_request), mysql_executor: &mut *mysql_connection, phantom: Default::default(), diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/beeble/beeble_webhook_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/beeble/beeble_webhook_handler.rs index 1ec9e0e8f1..044a44474e 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/beeble/beeble_webhook_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/beeble/beeble_webhook_handler.rs @@ -101,6 +101,7 @@ pub async fn beeble_webhook_handler( apriori_debug_log_event_token: Some(debug_log_event_token), maybe_creator_user_token: job.maybe_creator_user_token.as_ref(), debug_log_type: DebugLogType::BeebleWebhook, + maybe_log_level: None, message: &raw_body, mysql_executor: &mut *mysql_connection, phantom: Default::default(), diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/fal/process_failure/handle_failed_fal_webhook.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/fal/process_failure/handle_failed_fal_webhook.rs index 05961a8511..fc398deffa 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/fal/process_failure/handle_failed_fal_webhook.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/fal/process_failure/handle_failed_fal_webhook.rs @@ -52,6 +52,7 @@ pub async fn handle_failed_fal_webhook( apriori_debug_log_event_token: Some(debug_log_event_token), maybe_creator_user_token: job.maybe_creator_user_token.as_ref(), debug_log_type: DebugLogType::FalWebhook, + maybe_log_level: None, message: raw_body, mysql_executor: &mut **mysql_connection, phantom: Default::default(), diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/fal/process_success/handle_successful_fal_webhook.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/fal/process_success/handle_successful_fal_webhook.rs index 00d6a13e0f..260aef9093 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/fal/process_success/handle_successful_fal_webhook.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/webhooks/fal/process_success/handle_successful_fal_webhook.rs @@ -52,6 +52,7 @@ pub async fn handle_successful_fal_webhook( apriori_debug_log_event_token: Some(debug_log_event_token), maybe_creator_user_token: job.maybe_creator_user_token.as_ref(), debug_log_type: DebugLogType::FalWebhook, + maybe_log_level: None, message: raw_body, mysql_executor: &mut **mysql_connection, phantom: Default::default(), diff --git a/crates/service/web/storyteller_web/src/http_server/routes/application_routes/moderation_routes.rs b/crates/service/web/storyteller_web/src/http_server/routes/application_routes/moderation_routes.rs index c145ccf260..77fb14595e 100644 --- a/crates/service/web/storyteller_web/src/http_server/routes/application_routes/moderation_routes.rs +++ b/crates/service/web/storyteller_web/src/http_server/routes/application_routes/moderation_routes.rs @@ -36,7 +36,9 @@ use crate::http_server::endpoints::moderation::user::moderator_user_lookup_by_st use crate::http_server::endpoints::moderation::user::moderator_user_lookup_handler::moderator_user_lookup_handler; use crate::http_server::endpoints::moderation::alerts::moderation_send_alert_handler::moderation_send_alert_handler; use crate::http_server::endpoints::moderation::staff_audit_logs::moderator_list_staff_audit_logs_handler::moderator_list_staff_audit_logs_handler; +use crate::http_server::endpoints::moderation::debug_logs::moderation_list_all_debug_logs_handler::moderation_list_all_debug_logs_handler; use crate::http_server::endpoints::moderation::debug_logs::moderation_list_debug_logs_for_token_handler::moderation_list_debug_logs_for_token_handler; +use crate::http_server::endpoints::moderation::debug_logs::moderation_list_debug_logs_for_user_handler::moderation_list_debug_logs_for_user_handler; use crate::http_server::endpoints::moderation::jobs::moderation_get_job_by_token_handler::moderation_get_job_by_token_handler; use crate::http_server::endpoints::moderation::user_feature_flags::moderator_edit_user_feature_flags_handler::moderator_edit_user_feature_flags_handler; use crate::http_server::endpoints::moderation::user_feature_flags::moderator_list_all_available_user_feature_flags_handler::moderator_list_all_available_user_feature_flags_handler; @@ -102,6 +104,14 @@ pub fn add_moderator_routes (app: App) -> App .route(web::get().to(moderation_list_debug_logs_for_token_handler)) .route(web::head().to(|| HttpResponse::Ok())) ) + .service(web::resource("/list_all") + .route(web::get().to(moderation_list_all_debug_logs_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + .service(web::resource("/user_list/{user_token}") + .route(web::get().to(moderation_list_debug_logs_for_user_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) ) .service( web::scope("/user_emails")