Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import healthRouter from './routes/health.js';
import membersRouter from './routes/members.js';
import moderationRouter from './routes/moderation.js';
import notificationsRouter from './routes/notifications.js';
import performanceRouter from './routes/performance.js';
import ticketsRouter from './routes/tickets.js';
import webhooksRouter from './routes/webhooks.js';

Expand Down Expand Up @@ -59,6 +60,9 @@ 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);

// Notification webhook management routes — require API secret or OAuth2 JWT
router.use('/guilds', requireAuth(), auditLogMiddleware(), notificationsRouter);

Expand Down
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 @@ -5,6 +5,7 @@

import express from 'express';
import { error, info, warn } from '../logger.js';
import { PerformanceMonitor } from '../modules/performanceMonitor.js';
import apiRouter from './index.js';
import { rateLimit } from './middleware/rateLimit.js';
import { stopAuthCleanup } from './routes/auth.js';
Expand Down Expand Up @@ -69,6 +70,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
12 changes: 12 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { startGithubFeed, stopGithubFeed } from './modules/githubFeed.js';
import { checkMem0Health, markUnavailable } from './modules/memory.js';
import { startTempbanScheduler, stopTempbanScheduler } from './modules/moderation.js';
import { loadOptOuts } from './modules/optout.js';
import { PerformanceMonitor } from './modules/performanceMonitor.js';
import { startScheduler, stopScheduler } from './modules/scheduler.js';
import { startTriage, stopTriage } from './modules/triage.js';
import { startVoiceFlush, stopVoiceFlush } from './modules/voice.js';
Expand Down Expand Up @@ -126,6 +127,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();

/** @type {ReturnType<typeof setInterval> | null} Health degraded check interval */
let healthCheckInterval = null;

Expand Down Expand Up @@ -266,7 +270,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 @@ -309,6 +315,9 @@ async function gracefulShutdown(signal) {
stopTriage();
stopConversationCleanup();
stopTempbanScheduler();
stopScheduler();
stopGithubFeed();
perfMonitor.stop();
stopBotStatus();
stopVoiceFlush();

Expand Down Expand Up @@ -506,6 +515,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