← back to Norma
agents/shared/audit-logger.js
122 lines
/**
* Norma Audit Logger
*
* Logs every agent action to the sdcc_agent_actions table.
* Provides a full audit trail of posts, replies, monitors, and errors.
*/
const { query } = require('./db');
/**
* Log an agent action to the database.
*
* @param {Object} params
* @param {string} params.agent - Agent name (e.g. 'reddit-agent', 'twitter-agent')
* @param {string} params.actionType - Action type (e.g. 'post', 'reply', 'monitor', 'search')
* @param {string} params.platform - Platform name (e.g. 'reddit', 'twitter', 'discord')
* @param {string} [params.targetId] - Target identifier (post ID, thread ID, etc.)
* @param {string} [params.targetUrl] - URL of the target content
* @param {string} [params.content] - Content of the action (post body, reply text, etc.)
* @param {string} [params.petitionId] - Associated petition UUID, if applicable
* @param {string} [params.channel] - Channel or subreddit name
* @param {Object} [params.responseData] - JSON response data from platform API
* @param {string} [params.status='success'] - Action status: 'success', 'error', 'rate_limited', 'skipped'
* @param {string} [params.errorMessage] - Error message if status is 'error'
* @returns {Promise<Object>} The inserted row
*/
async function logAction({
agent,
actionType,
platform,
targetId = null,
targetUrl = null,
content = null,
petitionId = null,
channel = null,
responseData = null,
status = 'success',
errorMessage = null,
}) {
try {
const result = await query(
`INSERT INTO sdcc_agent_actions
(agent, action_type, platform, target_id, target_url, content, petition_id, channel, response_data, status, error_message)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING *`,
[
agent,
actionType,
platform,
targetId,
targetUrl,
content,
petitionId,
channel,
responseData ? JSON.stringify(responseData) : null,
status,
errorMessage,
]
);
return result.rows[0];
} catch (err) {
// Don't let audit logging failures crash the agent
console.error(`[${new Date().toISOString()}] [audit] Failed to log action:`, err.message);
console.error(`[${new Date().toISOString()}] [audit] Action was: ${agent}/${actionType}/${platform}`);
return null;
}
}
/**
* Get recent actions for an agent.
*
* @param {string} agent - Agent name to filter by
* @param {number} [limit=50] - Maximum number of actions to return
* @returns {Promise<Array>} Array of action rows, newest first
*/
async function getRecentActions(agent, limit = 50) {
try {
const { rows } = await query(
`SELECT id, agent, action_type, platform, target_id, target_url,
content, petition_id, channel, response_data, status,
error_message, created_at
FROM sdcc_agent_actions
WHERE agent = $1
ORDER BY created_at DESC
LIMIT $2`,
[agent, limit]
);
return rows;
} catch (err) {
console.error(`[${new Date().toISOString()}] [audit] Failed to get recent actions:`, err.message);
return [];
}
}
/**
* Get action counts by type for an agent within a time window.
* Useful for dashboards and status reports.
*
* @param {string} agent - Agent name
* @param {number} [hoursBack=24] - How many hours back to count
* @returns {Promise<Array>} Array of { action_type, platform, count, last_at }
*/
async function getActionSummary(agent, hoursBack = 24) {
try {
const { rows } = await query(
`SELECT action_type, platform, status, COUNT(*)::int as count,
MAX(created_at) as last_at
FROM sdcc_agent_actions
WHERE agent = $1 AND created_at > NOW() - INTERVAL '1 hour' * $2
GROUP BY action_type, platform, status
ORDER BY count DESC`,
[agent, hoursBack]
);
return rows;
} catch (err) {
console.error(`[${new Date().toISOString()}] [audit] Failed to get action summary:`, err.message);
return [];
}
}
module.exports = { logAction, getRecentActions, getActionSummary };