From 7c207c44c074cd386968a746f3c4dedd176fafa5 Mon Sep 17 00:00:00 2001 From: xaxxoo Date: Tue, 26 May 2026 20:55:02 +0100 Subject: [PATCH] feat: add /api/stellar/metrics endpoint for event-processing monitoring Resolves #68 - exposes getEventMetrics() from the Stellar event listener via GET /api/stellar/metrics, returning totalProcessed, totalErrors, errorRate, ledgerLag, processingRatePerMinute, lastDbOperationMs, and lastUpdated. Registers the router in index.ts and adds unit tests for metric shape and error-rate calculation. --- src/index.ts | 2 + src/routes/stellar.ts | 25 +++++++++++ src/stellar/__tests__/stellar.test.ts | 64 +++++++++++++++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 src/routes/stellar.ts diff --git a/src/index.ts b/src/index.ts index 3b36b9b..a0ff26e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,7 @@ import vaultRouter from './routes/vault' import analyticsRouter from './routes/analytics' import adminRouter from './routes/admin' import metricsRouter from './routes/metrics' +import stellarRouter from './routes/stellar' import { corsMiddleware, jsonBodyParser, payloadSizeErrorHandler, urlencodedBodyParser } from './middleware/corsandbody' // ── Readiness state ─────────────────────────────────────────────────────────── @@ -118,6 +119,7 @@ app.use('/api/deposit', depositRouter) app.use('/api/withdraw', withdrawRouter) app.use('/api/vault', vaultRouter) app.use('/api/analytics', analyticsRouter) +app.use('/api/stellar', stellarRouter) app.use('/metrics', metricsRouter) // Admin routes (protected, strictest rate limit) diff --git a/src/routes/stellar.ts b/src/routes/stellar.ts new file mode 100644 index 0000000..d56253d --- /dev/null +++ b/src/routes/stellar.ts @@ -0,0 +1,25 @@ +import { Router, Request, Response } from 'express'; +import { getEventMetrics } from '../stellar/events'; + +const router = Router(); + +/** + * GET /api/stellar/metrics + * Returns current event-processing metrics from the Stellar event listener. + */ +router.get('/metrics', (_req: Request, res: Response) => { + try { + const metrics = getEventMetrics(); + res.json({ + success: true, + data: metrics, + }); + } catch (error) { + res.status(500).json({ + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }); + } +}); + +export default router; diff --git a/src/stellar/__tests__/stellar.test.ts b/src/stellar/__tests__/stellar.test.ts index b7466b0..f3e4a3b 100644 --- a/src/stellar/__tests__/stellar.test.ts +++ b/src/stellar/__tests__/stellar.test.ts @@ -1,5 +1,6 @@ import { submitTransaction, waitForConfirmation } from '../client'; import { getOnChainBalance, triggerRebalance } from '../contract'; +import { getEventMetrics } from '../events'; import { Transaction } from '@stellar/stellar-sdk'; jest.mock('../client', () => ({ @@ -10,6 +11,10 @@ jest.mock('../client', () => ({ waitForConfirmation: jest.fn(), })); +jest.mock('../events', () => ({ + getEventMetrics: jest.fn(), +})); + describe('Stellar Integration - Unit Tests', () => { beforeEach(() => { jest.clearAllMocks(); @@ -114,6 +119,65 @@ describe('Stellar Integration - Unit Tests', () => { }); }); + describe('Event Metrics', () => { + it('should return current metrics with all required fields', () => { + const mockMetrics = { + totalProcessed: 42, + totalErrors: 2, + processingRatePerMinute: 10, + errorRate: 0.048, + ledgerLag: 3, + lastDbOperationMs: 12, + lastUpdated: new Date(), + }; + (getEventMetrics as jest.Mock).mockReturnValue(mockMetrics); + + const metrics = getEventMetrics(); + + expect(metrics.totalProcessed).toBe(42); + expect(metrics.totalErrors).toBe(2); + expect(metrics.processingRatePerMinute).toBe(10); + expect(metrics.errorRate).toBeCloseTo(0.048, 3); + expect(metrics.ledgerLag).toBe(3); + expect(metrics.lastDbOperationMs).toBe(12); + expect(metrics.lastUpdated).toBeInstanceOf(Date); + }); + + it('should return zero values for a fresh listener', () => { + const emptyMetrics = { + totalProcessed: 0, + totalErrors: 0, + processingRatePerMinute: 0, + errorRate: 0, + ledgerLag: 0, + lastDbOperationMs: 0, + lastUpdated: new Date(), + }; + (getEventMetrics as jest.Mock).mockReturnValue(emptyMetrics); + + const metrics = getEventMetrics(); + + expect(metrics.totalProcessed).toBe(0); + expect(metrics.errorRate).toBe(0); + }); + + it('should compute errorRate as totalErrors / totalProcessed', () => { + const totalProcessed = 100; + const totalErrors = 5; + const errorRate = totalProcessed > 0 ? totalErrors / totalProcessed : 0; + + expect(errorRate).toBeCloseTo(0.05, 3); + }); + + it('should return zero errorRate when no events processed', () => { + const totalProcessed = 0; + const totalErrors = 0; + const errorRate = totalProcessed > 0 ? totalErrors / totalProcessed : 0; + + expect(errorRate).toBe(0); + }); + }); + describe('Event Parsing', () => { it('should parse deposit event', () => { const mockEvent = {