Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import membersRouter from './routes/members.js';
import moderationRouter from './routes/moderation.js';
import ticketsRouter from './routes/tickets.js';
import webhooksRouter from './routes/webhooks.js';
import performanceRouter from './routes/performance.js';

const router = Router();

Expand Down Expand Up @@ -58,7 +59,11 @@ router.use('/moderation', requireAuth(), auditLogMiddleware(), moderationRouter)
// GET-only; no audit middleware needed (reads are not mutating actions)
router.use('/guilds', requireAuth(), auditLogRouter);

// Performance metrics — require x-api-secret (authenticated via route handler)
router.use('/performance', performanceRouter);

// Webhook routes — require API secret or OAuth2 JWT (endpoint further restricts to api-secret)
router.use('/webhooks', requireAuth(), webhooksRouter);

export default router;

122 changes: 122 additions & 0 deletions src/api/routes/performance.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* Performance Metrics Route
*
* Returns bot performance metrics including memory usage, CPU utilization,
* response time statistics, and configurable alert thresholds.
*
* All endpoints require x-api-secret authentication.
*/

import { Router } from 'express';
import { PerformanceMonitor } from '../../modules/performanceMonitor.js';
import { isValidSecret } from '../middleware/auth.js';

const router = Router();

/**
* @openapi
* /performance:
* get:
* tags:
* - Performance
* summary: Get performance metrics snapshot
* description: >
* Returns the full performance snapshot including current stats,
* time-series data for memory/CPU, response time samples and summary,
* and configured alert thresholds. Requires x-api-secret header.
* parameters:
* - in: header
* name: x-api-secret
* schema:
* type: string
* required: true
* responses:
* "200":
* description: Performance snapshot
* "401":
* description: Unauthorized
*/
router.get('/', (req, res) => {
if (!isValidSecret(req.headers['x-api-secret'])) {
return res.status(401).json({ error: 'Unauthorized' });
}

const monitor = PerformanceMonitor.getInstance();
const snapshot = monitor.getSnapshot();
res.json(snapshot);
});

/**
* @openapi
* /performance/thresholds:
* get:
* tags:
* - Performance
* summary: Get alert thresholds
* security:
* - apiSecret: []
* responses:
* "200":
* description: Current thresholds
* put:
* tags:
* - Performance
* summary: Update alert thresholds
* description: Partial update — only supplied fields are changed.
* security:
* - apiSecret: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* memoryHeapMb:
* type: number
* memoryRssMb:
* type: number
* cpuPercent:
* type: number
* responseTimeMs:
* type: number
* responses:
* "200":
* description: Updated thresholds
*/
router.get('/thresholds', (req, res) => {
if (!isValidSecret(req.headers['x-api-secret'])) {
return res.status(401).json({ error: 'Unauthorized' });
}

const monitor = PerformanceMonitor.getInstance();
res.json(monitor.getThresholds());
});

router.put('/thresholds', (req, res) => {
if (!isValidSecret(req.headers['x-api-secret'])) {
return res.status(401).json({ error: 'Unauthorized' });
}

const allowed = new Set(['memoryHeapMb', 'memoryRssMb', 'cpuPercent', 'responseTimeMs']);
const update = {};

for (const [key, val] of Object.entries(req.body ?? {})) {
if (!allowed.has(key)) continue;
const num = Number(val);
if (!Number.isFinite(num) || num <= 0) {
return res.status(400).json({ error: `Invalid value for ${key}: must be a positive number` });
}
update[key] = num;
}

if (Object.keys(update).length === 0) {
return res.status(400).json({ error: 'No valid threshold fields provided' });
}

const monitor = PerformanceMonitor.getInstance();
monitor.setThresholds(update);
res.json(monitor.getThresholds());
});

export default router;
12 changes: 12 additions & 0 deletions src/api/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import express from 'express';
import { error, info, warn } from '../logger.js';
import apiRouter from './index.js';
import { PerformanceMonitor } from '../modules/performanceMonitor.js';
import { rateLimit } from './middleware/rateLimit.js';
import { stopAuthCleanup } from './routes/auth.js';
import { swaggerSpec } from './swagger.js';
Expand Down Expand Up @@ -68,6 +69,17 @@ export function createApp(client, dbPool) {
// Raw OpenAPI spec (JSON) — public for Mintlify
app.get('/api/docs.json', (_req, res) => res.json(swaggerSpec));

// Response time tracking for performance monitoring
app.use('/api/v1', (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
const label = `${req.method} ${req.path}`;
PerformanceMonitor.getInstance().recordResponseTime(label, duration, 'api');
});
next();
});

// Mount API routes under /api/v1
app.use('/api/v1', apiRouter);

Expand Down
10 changes: 10 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import { closeRedisClient as closeRedis, initRedis } from './redis.js';
import { pruneOldLogs } from './transports/postgres.js';
import { stopCacheCleanup } from './utils/cache.js';
import { HealthMonitor } from './utils/health.js';
import { PerformanceMonitor } from './modules/performanceMonitor.js';
import { loadCommandsFromDirectory } from './utils/loadCommands.js';
import { getPermissionError, hasPermission } from './utils/permissions.js';
import { registerCommands } from './utils/registerCommands.js';
Expand Down Expand Up @@ -119,6 +120,9 @@ client.commands = new Collection();
// Initialize health monitor
const healthMonitor = HealthMonitor.getInstance();

// Initialize performance monitor (singleton; start() called in ClientReady handler)
const perfMonitor = PerformanceMonitor.getInstance();

/**
* Save conversation history to disk
*/
Expand Down Expand Up @@ -232,7 +236,9 @@ client.on('interactionCreate', async (interaction) => {
return;
}

const _cmdStart = Date.now();
await command.execute(interaction);
perfMonitor.recordResponseTime(commandName, Date.now() - _cmdStart, 'command');
info('Command executed', {
command: commandName,
user: interaction.user.tag,
Expand Down Expand Up @@ -277,6 +283,7 @@ async function gracefulShutdown(signal) {
stopTempbanScheduler();
stopScheduler();
stopGithubFeed();
perfMonitor.stop();

// 1.5. Stop API server (drain in-flight HTTP requests before closing DB)
try {
Expand Down Expand Up @@ -459,6 +466,9 @@ async function startup() {
// Register event handlers with live config reference
registerEventHandlers(client, config, healthMonitor);

// Start performance monitor
perfMonitor.start();

// Start triage module (per-channel message classification + response)
await startTriage(client, config, healthMonitor);

Expand Down
Loading
Loading