mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-18 00:46:45 +00:00
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:
parent
dfc6df7f13
commit
2c42cf65cb
3 changed files with 203 additions and 1 deletions
|
|
@ -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",
|
||||
|
|
|
|||
169
packages/super-sync-server/scripts/monitor.ts
Normal file
169
packages/super-sync-server/scripts/monitor.ts
Normal 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();
|
||||
|
|
@ -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');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue