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
1 change: 0 additions & 1 deletion backend/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ export default {
'tests/oauth.integration.test.ts',
'tests/generator.service.test.ts',
'tests/gas-estimation.test.ts',
'tests/vulnerability-scanner.test.ts',
'tests/generator.websocket.test.ts',
'tests/generator.rate-limit.test.ts'
],
Expand Down
272 changes: 237 additions & 35 deletions backend/src/routes/admin/dlq.routes.ts
Original file line number Diff line number Diff line change
@@ -1,70 +1,272 @@
import { Router } from 'express';
import { Router, Request, Response } from 'express';
import {
getDLQMetrics,
inspectDLQ,
purgeDLQ,
replayAllDLQJobs,
getDLQMetrics,
replayDLQJob,
replayAllDLQJobs,
purgeDLQ,
DLQJobRecord
} from '../../services/dlq.service.js';
import logger from '../../utils/logger.js';

const router: ReturnType<typeof Router> = Router();

// GET /admin/dlq - List / inspect DLQ jobs
router.get('/', async (req, res) => {
/**
* @route GET /api/v1/admin/dlq/metrics
* @desc Get DLQ metrics including total count and per-queue breakdown
*/
router.get('/metrics', async (_req: Request, res: Response) => {
try {
const queue = typeof req.query.queue === 'string' ? req.query.queue : undefined;
const limit = req.query.limit ? Number(req.query.limit) : undefined;
const records = await inspectDLQ({ queue, limit });
res.json({ records, count: records.length });
const metrics = await getDLQMetrics();

res.json({
status: 'success',
data: { metrics }
});
} catch (error: any) {
res.status(500).json({ error: error.message });
logger.error('Failed to get DLQ metrics:', error);
res.status(500).json({
status: 'error',
error: 'Failed to retrieve DLQ metrics'
});
}
});

// GET /admin/dlq/metrics - Get DLQ metrics & alert status
router.get('/metrics', async (req, res) => {
/**
* @route GET /api/v1/admin/dlq/jobs
* @desc Inspect jobs in the DLQ with optional filtering
*/
router.get('/jobs', async (req: Request, res: Response) => {
try {
const metrics = await getDLQMetrics();
res.json(metrics);
const { queue, limit } = req.query;

const filter: { queue?: string; limit?: number } = {};
if (queue && typeof queue === 'string') {
filter.queue = queue;
}
if (limit && typeof limit === 'string') {
const parsedLimit = parseInt(limit, 10);
if (!isNaN(parsedLimit) && parsedLimit > 0) {
filter.limit = parsedLimit;
}
}

const jobs = await inspectDLQ(filter);

res.json({
status: 'success',
data: {
jobs,
count: jobs.length,
filter
}
});
} catch (error: any) {
logger.error('Failed to inspect DLQ jobs:', error);
res.status(500).json({
status: 'error',
error: 'Failed to inspect DLQ jobs'
});
}
});

/**
* @route GET /api/v1/admin/dlq/jobs/:dlqId
* @desc Get a specific DLQ job by ID
*/
router.get('/jobs/:dlqId', async (req: Request, res: Response) => {
try {
const { dlqId } = req.params;
const jobs = await inspectDLQ();
const job = jobs.find(j => j.dlqId === dlqId);

if (!job) {
res.status(404).json({
status: 'error',
error: `DLQ job not found: ${dlqId}`
});
return;
}

res.json({
status: 'success',
data: { job }
});
} catch (error: any) {
res.status(500).json({ error: error.message });
logger.error(`Failed to get DLQ job ${req.params.dlqId}:`, error);
res.status(500).json({
status: 'error',
error: 'Failed to retrieve DLQ job'
});
}
});

// POST /admin/dlq/replay/:dlqId - Replay a specific job from DLQ
router.post('/replay/:dlqId', async (req, res) => {
/**
* @route POST /api/v1/admin/dlq/jobs/:dlqId/replay
* @desc Replay a single DLQ job back to its original queue
*/
router.post('/jobs/:dlqId/replay', async (req: Request, res: Response) => {
try {
const { dlqId } = req.params;
const result = await replayDLQJob(dlqId);
const result = await replayDLQJob(dlqId as string);

if (!result.success) {
return res.status(404).json({ error: result.error });
res.status(400).json({
status: 'error',
error: result.error || 'Failed to replay DLQ job'
});
return;
}

res.json({
status: 'success',
data: {
message: 'Job successfully replayed',
replayedJobId: result.replayedJobId
}
});
} catch (error: any) {
logger.error(`Failed to replay DLQ job ${req.params.dlqId}:`, error);
res.status(500).json({
status: 'error',
error: 'Failed to replay DLQ job'
});
}
});

/**
* @route POST /api/v1/admin/dlq/replay
* @desc Replay all DLQ jobs or all jobs for a specific queue
*/
router.post('/replay', async (req: Request, res: Response) => {
try {
const { queueName } = req.body;

const result = await replayAllDLQJobs(queueName);

res.json({
status: 'success',
data: {
message: `Replayed ${result.replayedCount} jobs`,
replayedCount: result.replayedCount,
errors: result.errors,
queueName: queueName || 'all'
}
});
} catch (error: any) {
logger.error('Failed to replay DLQ jobs:', error);
res.status(500).json({
status: 'error',
error: 'Failed to replay DLQ jobs'
});
}
});

/**
* @route DELETE /api/v1/admin/dlq/purge
* @desc Purge DLQ jobs from storage
*/
router.delete('/purge', async (req: Request, res: Response) => {
try {
const { queueName, confirm } = req.body;

if (!confirm) {
res.status(400).json({
status: 'error',
error: 'Purge operation must be confirmed with confirm: true'
});
return;
}
res.json({ message: 'Job replayed successfully', replayedJobId: result.replayedJobId });

const result = await purgeDLQ(queueName);

res.json({
status: 'success',
data: {
message: `Purged ${result.purgedCount} jobs`,
purgedCount: result.purgedCount,
queueName: queueName || 'all'
}
});
} catch (error: any) {
res.status(500).json({ error: error.message });
logger.error('Failed to purge DLQ jobs:', error);
res.status(500).json({
status: 'error',
error: 'Failed to purge DLQ jobs'
});
}
});

// POST /admin/dlq/replay - Replay all jobs (or jobs for specific queue)
router.post('/replay', async (req, res) => {
/**
* @route GET /api/v1/admin/dlq/health
* @desc Get DLQ health status and alerting information
*/
router.get('/health', async (_req: Request, res: Response) => {
try {
const queue = typeof req.body.queue === 'string' ? req.body.queue : undefined;
const result = await replayAllDLQJobs(queue);
res.json(result);
const metrics = await getDLQMetrics();

const health = {
status: metrics.isAlerting ? 'alerting' : 'healthy',
totalJobs: metrics.totalCount,
threshold: metrics.threshold,
isAlerting: metrics.isAlerting,
queueBreakdown: metrics.perQueue,
timestamp: new Date().toISOString()
};

res.json({
status: 'success',
data: { health }
});
} catch (error: any) {
res.status(500).json({ error: error.message });
logger.error('Failed to get DLQ health:', error);
res.status(500).json({
status: 'error',
error: 'Failed to retrieve DLQ health status'
});
}
});

// POST /admin/dlq/purge - Purge jobs from DLQ
router.post('/purge', async (req, res) => {
/**
* @route GET /api/v1/admin/dlq/queues
* @desc Get list of available queues in the DLQ system
*/
router.get('/queues', async (_req: Request, res: Response) => {
try {
const queue = typeof req.body.queue === 'string' ? req.body.queue : undefined;
const result = await purgeDLQ(queue);
res.json(result);
const jobs = await inspectDLQ();
const queueNames = [...new Set(jobs.map(job => job.originalQueue))];

const queueStats = queueNames.map(queueName => {
const queueJobs = jobs.filter(job => job.originalQueue === queueName);
return {
name: queueName,
jobCount: queueJobs.length,
oldestJob: queueJobs.length > 0 ?
queueJobs.reduce((oldest, job) =>
new Date(job.failedAt) < new Date(oldest.failedAt) ? job : oldest
).failedAt : null,
newestJob: queueJobs.length > 0 ?
queueJobs.reduce((newest, job) =>
new Date(job.failedAt) > new Date(newest.failedAt) ? job : newest
).failedAt : null
};
});

res.json({
status: 'success',
data: {
queues: queueStats,
totalQueues: queueNames.length,
totalJobs: jobs.length
}
});
} catch (error: any) {
res.status(500).json({ error: error.message });
logger.error('Failed to get DLQ queues:', error);
res.status(500).json({
status: 'error',
error: 'Failed to retrieve DLQ queue information'
});
}
});

export default router;
export default router;
4 changes: 4 additions & 0 deletions backend/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ import simulatorIdeasRouter from '../simulator/simulator.routes.js';
import deployRouter from './deploy.routes.js';
import webhooksRouter from './webhooks.js';
import adminDLQRouter from './admin/dlq.routes.js';
import policyRouter from './policy/policy.routes.js';
import storageRouter from './storage.routes.js';

const router: ReturnType<typeof Router> = Router();

Expand Down Expand Up @@ -71,6 +73,8 @@ router.use('/export', exportRouter);
router.use('/deploy', deployRouter);
router.use('/webhooks', webhooksRouter);
router.use('/admin/dlq', adminDLQRouter);
router.use('/policy', policyRouter);
router.use('/storage', storageRouter);
router.use('/user', userRouter);
router.use('/metrics', metricsRouter);
router.use('/dependencies', dependenciesRouter);
Expand Down
Loading