feat(super-sync-server): Add basic monitoring tool

This commit introduces a lightweight monitoring CLI tool for the super-sync-server.

Changes include:
- Enhanced logging:  now supports writing logs to a file () when  is set, enabling persistent log storage.
- New monitoring script:  provides commands for:
    - : Displays system vitals (CPU, RAM) and database connectivity/size.
    - : Shows top 20 users by data storage usage within the database.
    - : Allows tailing, searching, and filtering server logs from the file.
-  update: Added a  script to easily run the tool.
This commit is contained in:
Johannes Millan 2025-12-11 14:48:23 +01:00
parent dfc6df7f13
commit 2c42cf65cb
3 changed files with 203 additions and 1 deletions

View file

@ -9,7 +9,8 @@
"dev": "nodemon --watch src --ext ts --exec ts-node src/index.ts",
"test": "vitest run",
"delete-user": "ts-node scripts/delete-user.ts",
"clear-data": "ts-node scripts/clear-data.ts"
"clear-data": "ts-node scripts/clear-data.ts",
"monitor": "ts-node scripts/monitor.ts"
},
"dependencies": {
"@fastify/cors": "^11.1.0",

View file

@ -0,0 +1,169 @@
import { prisma, disconnectDb } from '../src/db';
import * as os from 'os';
import * as fs from 'fs';
import * as path from 'path';
import * as readline from 'readline';
const LOG_FILE_PATH = path.join(process.cwd(), 'logs', 'app.log');
const formatBytes = (bytes: number, decimals = 2) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
};
const showStats = async () => {
console.log('\n--- System Vitals ---');
console.log(`Hostname: ${os.hostname()}`);
console.log(`OS: ${os.type()} ${os.release()} (${os.arch()})`);
console.log(`CPUs: ${os.cpus().length}`);
const loadAvg = os.loadavg();
console.log(
`Load Avg: ${loadAvg[0].toFixed(2)}, ${loadAvg[1].toFixed(2)}, ${loadAvg[2].toFixed(2)}`,
);
const totalMem = os.totalmem();
const freeMem = os.freemem();
const usedMem = totalMem - freeMem;
console.log(`Memory: ${formatBytes(usedMem)} used / ${formatBytes(totalMem)} total`);
console.log('\n--- Database Connection ---');
try {
await prisma.$queryRaw`SELECT 1`;
console.log('Status: Connected ✅');
// Get DB Size
const dbSizeResult: any[] = await prisma.$queryRaw`
SELECT pg_size_pretty(pg_database_size(current_database())) as size;
`;
console.log(`DB Size: ${dbSizeResult[0]?.size}`);
} catch (error) {
console.log('Status: Disconnected ❌');
console.error('Error:', error);
}
};
const showUsage = async () => {
console.log('\n--- User Storage Usage (Top 20) ---');
try {
// Calculate size of operations and snapshot data per user
const users: any[] = await prisma.$queryRaw`
SELECT
u.id,
u.email,
(
COALESCE(SUM(pg_column_size(o.payload)), 0) +
COALESCE(SUM(pg_column_size(s.snapshot_data)), 0)
) as total_bytes
FROM users u
LEFT JOIN operations o ON u.id = o.user_id
LEFT JOIN user_sync_state s ON u.id = s.user_id
GROUP BY u.id
ORDER BY total_bytes DESC
LIMIT 20;
`;
if (users.length === 0) {
console.log('No users found.');
} else {
console.table(
users.map((u) => ({
ID: u.id,
Email: u.email,
Usage: formatBytes(Number(u.total_bytes)),
})),
);
}
} catch (error) {
console.error('Error fetching usage data:', error);
}
};
const showLogs = async (args: string[]) => {
console.log('\n--- Server Logs ---');
if (!fs.existsSync(LOG_FILE_PATH)) {
console.error(`Log file not found at: ${LOG_FILE_PATH}`);
console.error(
'Ensure LOG_TO_FILE=true is set in .env and the server has written logs.',
);
return;
}
const searchIndex = args.indexOf('--search');
const searchTerm = searchIndex !== -1 ? args[searchIndex + 1] : null;
const tailIndex = args.indexOf('--tail');
const tailCount = tailIndex !== -1 ? parseInt(args[tailIndex + 1], 10) : 100;
const onlyErrors = args.includes('--error');
const fileStream = fs.createReadStream(LOG_FILE_PATH);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity,
});
const lines: string[] = [];
for await (const line of rl) {
let include = true;
if (onlyErrors && !line.includes('"level":"ERROR"') && !line.includes('[ERROR]')) {
include = false;
}
if (searchTerm && !line.toLowerCase().includes(searchTerm.toLowerCase())) {
include = false;
}
if (include) {
lines.push(line);
if (lines.length > tailCount) {
lines.shift(); // Keep only the last N lines
}
}
}
lines.forEach((line) => console.log(line));
};
const main = async () => {
const args = process.argv.slice(2);
const command = args[0];
try {
switch (command) {
case 'stats':
await showStats();
break;
case 'usage':
await showUsage();
break;
case 'logs':
await showLogs(args);
break;
default:
console.log('SuperSync Monitor CLI');
console.log('Usage: npm run monitor -- <command> [flags]');
console.log('\nCommands:');
console.log(' stats Show system vitals and DB status');
console.log(' usage Show top 20 users by storage usage');
console.log(' logs Show server logs');
console.log(' --tail <n> Show last n lines (default 100)');
console.log(' --search "s" Filter logs by term');
console.log(' --error Show only errors');
break;
}
} catch (err) {
console.error('Unexpected error:', err);
} finally {
await disconnectDb();
}
};
main();

View file

@ -1,5 +1,30 @@
import * as fs from 'fs';
import * as path from 'path';
const getTimestamp = (): string => new Date().toISOString();
let logFileStream: fs.WriteStream | null = null;
if (process.env.LOG_TO_FILE === 'true') {
const logDir = path.join(process.cwd(), 'logs');
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir);
}
logFileStream = fs.createWriteStream(path.join(logDir, 'app.log'), { flags: 'a' });
}
const writeToFile = (level: string, message: string, ...args: unknown[]) => {
if (logFileStream) {
const logEntry = JSON.stringify({
timestamp: getTimestamp(),
level,
message,
args: args.length ? args : undefined,
});
logFileStream.write(logEntry + '\n');
}
};
export interface AuditLogEntry {
event: string;
userId: number;
@ -17,16 +42,20 @@ export const Logger = {
debug: (message: string, ...args: unknown[]): void => {
if (process.env.LOG_LEVEL === 'debug') {
console.log(`${getTimestamp()} [DEBUG] ${message}`, ...args);
writeToFile('DEBUG', message, ...args);
}
},
info: (message: string, ...args: unknown[]): void => {
console.log(`${getTimestamp()} [INFO] ${message}`, ...args);
writeToFile('INFO', message, ...args);
},
warn: (message: string, ...args: unknown[]): void => {
console.warn(`${getTimestamp()} [WARN] ${message}`, ...args);
writeToFile('WARN', message, ...args);
},
error: (message: string, ...args: unknown[]): void => {
console.error(`${getTimestamp()} [ERROR] ${message}`, ...args);
writeToFile('ERROR', message, ...args);
},
/**
@ -46,5 +75,8 @@ export const Logger = {
...entry,
};
console.log(JSON.stringify(logEntry));
if (logFileStream) {
logFileStream.write(JSON.stringify(logEntry) + '\n');
}
},
};