systemprompt-logging 0.2.2

Tracing and audit infrastructure for systemprompt.io AI governance. Structured events, five-point audit traces, and SIEM-ready JSON output — part of the MCP governance pipeline.
Documentation
use anyhow::Result;
use chrono::{DateTime, Utc};
use sqlx::FromRow;
use systemprompt_identifiers::LogId;

use super::{LogEntry, LogLevel};

#[derive(Debug, FromRow)]
pub struct LogRow {
    pub id: LogId,
    pub timestamp: DateTime<Utc>,
    pub level: String,
    pub module: String,
    pub message: String,
    pub metadata: Option<String>,
    pub user_id: Option<String>,
    pub session_id: Option<String>,
    pub task_id: Option<String>,
    pub trace_id: Option<String>,
    pub context_id: Option<String>,
    pub client_id: Option<String>,
}

impl LogRow {
    pub fn from_json_row(row: &systemprompt_database::JsonRow) -> Result<Self> {
        use anyhow::anyhow;

        let id = row
            .get("id")
            .and_then(|v| v.as_str())
            .map(LogId::new)
            .ok_or_else(|| anyhow!("Missing id"))?;

        let timestamp = row
            .get("timestamp")
            .and_then(systemprompt_database::parse_database_datetime)
            .ok_or_else(|| anyhow!("Invalid timestamp"))?;

        let level = row
            .get("level")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow!("Missing level"))?
            .to_string();

        let module = row
            .get("module")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow!("Missing module"))?
            .to_string();

        let message = row
            .get("message")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow!("Missing message"))?
            .to_string();

        let metadata = row
            .get("metadata")
            .and_then(|v| v.as_str())
            .map(String::from);

        let user_id = row
            .get("user_id")
            .and_then(|v| v.as_str())
            .map(String::from);

        let session_id = row
            .get("session_id")
            .and_then(|v| v.as_str())
            .map(String::from);

        let task_id = row
            .get("task_id")
            .and_then(|v| v.as_str())
            .map(String::from);

        let trace_id = row
            .get("trace_id")
            .and_then(|v| v.as_str())
            .map(String::from);

        let context_id = row
            .get("context_id")
            .and_then(|v| v.as_str())
            .map(String::from);

        let client_id = row
            .get("client_id")
            .and_then(|v| v.as_str())
            .map(String::from);

        Ok(Self {
            id,
            timestamp,
            level,
            module,
            message,
            metadata,
            user_id,
            session_id,
            task_id,
            trace_id,
            context_id,
            client_id,
        })
    }
}

impl From<LogRow> for LogEntry {
    fn from(row: LogRow) -> Self {
        let level = row.level.parse().unwrap_or(LogLevel::Info);

        Self {
            id: row.id,
            timestamp: row.timestamp,
            level,
            module: row.module,
            message: row.message,
            metadata: row.metadata.and_then(|s| {
                serde_json::from_str(&s)
                    .map_err(|e| {
                        tracing::warn!(error = %e, "Malformed log metadata JSON");
                        e
                    })
                    .ok()
            }),
            user_id: row.user_id.map_or_else(
                systemprompt_identifiers::UserId::system,
                systemprompt_identifiers::UserId::new,
            ),
            session_id: row.session_id.map_or_else(
                systemprompt_identifiers::SessionId::system,
                systemprompt_identifiers::SessionId::new,
            ),
            task_id: row.task_id.map(systemprompt_identifiers::TaskId::new),
            trace_id: row.trace_id.map_or_else(
                systemprompt_identifiers::TraceId::system,
                systemprompt_identifiers::TraceId::new,
            ),
            context_id: row.context_id.map(systemprompt_identifiers::ContextId::new),
            client_id: row.client_id.map(systemprompt_identifiers::ClientId::new),
        }
    }
}