debug logs: new field, new queries, new APIs

This commit is contained in:
Brandon Thomas committed 2026-07-02 15:54:59 -04:00
1 parent cbd3710f4f
commit 197ec95012
32 files changed
+848 -56

No files matched your search

@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE debug_logs
DROP COLUMN maybe_log_level;
@@ -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;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE debug_logs
DROP INDEX index_maybe_log_level;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE debug_logs
ADD INDEX index_maybe_log_level (maybe_log_level);
@@ -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)
@@ -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<DebugLogLevel>,
pub maybe_creator_user_token: Option<UserToken>,
pub message: String,
pub created_at: DateTime<Utc>,
}
@@ -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;
@@ -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<String>,
/// Cursor for pagination. Pass the `next_cursor` from a previous response.
pub cursor: Option<u64>,
/// Max rows per page (default 50, max 200).
pub limit: Option<u32>,
}
impl ModerationListAllDebugLogsQueryParams {
/// Parse the comma-separated `severities` parameter.
/// Returns `None` when the parameter is absent or empty.
pub fn parsed_severities(&self) -> Result<Option<Vec<DebugLogLevel>>, 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::<Result<Vec<DebugLogLevel>, 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<ModerationListAllDebugLogsEntry>,
/// Cursor for the next page. `None` if there are no more results.
pub next_cursor: Option<u64>,
}
/// 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<DebugLogLevel>,
pub maybe_creator_user_token: Option<UserToken>,
pub message: String,
pub created_at: DateTime<Utc>,
pub maybe_user: Option<ModerationListAllDebugLogsUser>,
}
/// 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,
}
@@ -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<u32>,
}
// ── Response ──
#[derive(Serialize, ToSchema)]
pub struct ModerationListDebugLogsForTokenSuccessResponse {
pub success: bool,
pub debug_logs: Vec<ModerationDebugLogEntry>,
}
@@ -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<u64>,
/// Max rows per page (default 50, max 200).
pub limit: Option<u32>,
}
// ── Response ──
#[derive(Serialize, ToSchema)]
pub struct ModerationListDebugLogsForUserSuccessResponse {
pub success: bool,
/// Most recent first.
pub debug_logs: Vec<ModerationDebugLogEntry>,
/// Cursor for the next page. `None` if there are no more results.
pub next_cursor: Option<u64>,
}
@@ -1,4 +1,5 @@
pub mod alerts;
pub mod debug_logs;
pub mod jobs;
pub mod user;
pub mod user_referrals;
@@ -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<DebugLogLevel>,
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);
@@ -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<DebugLogLevel>,
/// 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,
)
@@ -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<Vec<DebugLogLevel>>,
/// 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<u64>,
pub limit: Option<u32>,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
pub struct ListAllDebugLogsResult {
pub debug_logs: Vec<AllDebugLogRow>,
/// Cursor for the next page. `None` if there are no more results.
pub next_cursor: Option<u64>,
}
/// 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<DebugLogLevel>,
pub maybe_creator_user_token: Option<UserToken>,
pub message: String,
pub created_at: DateTime<Utc>,
// Joined user fields (present when the creator user exists).
pub maybe_user_display_name: Option<String>,
pub maybe_user_username: Option<String>,
pub maybe_user_gravatar_hash: Option<String>,
}
/// 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<ListAllDebugLogsResult, sqlx::Error>
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<MySql> = 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<MySqlRow> = query_builder
.build()
.fetch_all(args.mysql_executor)
.await?;
let mut debug_logs = raw_rows
.into_iter()
.map(row_from_mysql_row)
.collect::<Result<Vec<AllDebugLogRow>, 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<AllDebugLogRow, sqlx::Error> {
let created_at: DateTime<Utc> = 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")?,
})
}
@@ -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<DebugLogLevel>,
pub maybe_creator_user_token: Option<UserToken>,
pub message: String,
pub created_at: DateTime<Utc>,
@@ -34,6 +36,7 @@ struct RawDebugLogRow {
id: u64,
event_token: DebugLogEventToken,
debug_log_type: DebugLogType,
maybe_log_level: Option<DebugLogLevel>,
maybe_creator_user_token: Option<UserToken>,
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(),
@@ -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<u64>,
pub limit: Option<u32>,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
pub struct ListDebugLogsForUserResult {
pub debug_logs: Vec<UserDebugLogRow>,
/// Cursor for the next page. `None` if there are no more results.
pub next_cursor: Option<u64>,
}
#[derive(Debug)]
pub struct UserDebugLogRow {
pub id: u64,
pub event_token: DebugLogEventToken,
pub debug_log_type: DebugLogType,
pub maybe_log_level: Option<DebugLogLevel>,
pub maybe_creator_user_token: Option<UserToken>,
pub message: String,
pub created_at: DateTime<Utc>,
}
#[derive(Debug)]
struct RawUserDebugLogRow {
id: u64,
event_token: DebugLogEventToken,
debug_log_type: DebugLogType,
maybe_log_level: Option<DebugLogLevel>,
maybe_creator_user_token: Option<UserToken>,
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<ListDebugLogsForUserResult, sqlx::Error>
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<UserDebugLogRow> = 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,
})
}
@@ -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;
@@ -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<Self, EnumError> {
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<Self> {
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);
}
}
}
}
@@ -1 +1,2 @@
pub mod debug_log_level;
pub mod debug_log_type;
@@ -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,
@@ -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;
@@ -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<ModerationListAllDebugLogsQueryParams>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ModerationListAllDebugLogsSuccessResponse>, 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<ModerationListAllDebugLogsEntry> = 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,
}))
}
@@ -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<u32>,
}
// ── Response ──
#[derive(Serialize, ToSchema)]
pub struct ListDebugLogsSuccessResponse {
pub success: bool,
pub debug_logs: Vec<DebugLogEntry>,
}
#[derive(Serialize, ToSchema)]
pub struct DebugLogEntry {
pub id: u64,
pub event_token: DebugLogEventToken,
pub debug_log_type: DebugLogType,
pub maybe_creator_user_token: Option<UserToken>,
pub message: String,
pub created_at: DateTime<Utc>,
}
// ── 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<ListDebugLogsPathInfo>,
query: web::Query<ListDebugLogsQueryParams>,
path: web::Path<ModerationListDebugLogsForTokenPathInfo>,
query: web::Query<ModerationListDebugLogsForTokenQueryParams>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ListDebugLogsSuccessResponse>, CommonWebError> {
let _user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
) -> Result<Json<ModerationListDebugLogsForTokenSuccessResponse>, 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<DebugLogEntry> = rows.into_iter().map(|row| {
DebugLogEntry {
let debug_logs: Vec<ModerationDebugLogEntry> = 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,
}))
@@ -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<ModerationListDebugLogsForUserPathInfo>,
query: web::Query<ModerationListDebugLogsForUserQueryParams>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ModerationListDebugLogsForUserSuccessResponse>, 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<ModerationDebugLogEntry> = 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,
}))
}
@@ -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(),
@@ -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(),
@@ -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(),
@@ -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(),
@@ -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(),
@@ -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(),
@@ -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(),
@@ -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<T, B> (app: App<T>) -> App<T>
.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")