Skip to content
Open
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
387 changes: 46 additions & 341 deletions .github/workflows/ci.yml

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,21 @@ pact.log



# Test snapshots
**/__snapshots__/
**/*.snap

# Load test output
tests/load/results/*.json
benchmarks/results/*.json

# Performance baselines
tests/load/baselines/*.json

# Pact test logs
pact.log
*.pact.log

# Temporary files
*.tmp
*.temp
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
"test:load:breakpoint": "k6 run -e SCENARIO=breakpoint tests/load/api.js",
"test:load:legacy": "k6 run tests/load/k6/load_test_scenarios.js",
"test:bench": "node tests/load/autocannon/benchmark.js",
"test:perf:regression": "node tests/load/performance-regression.js",
"test:perf:save-baseline": "node -e \"const fs=require('fs');const p=require('path').join(__dirname,'tests','load','baselines','baseline.json');fs.writeFileSync(p,JSON.stringify({timestamp:new Date().toISOString(),scenarios:[]},null,2))\"",
"bench:soroban-gas": "node benchmarks/soroban-gas-bench.js",
"sdk:generate": "echo 'Start dev server first (npm run dev), then run: openapi-generator-cli generate -i http://localhost:3000/docs/openapi.json -c sdk-config.yaml -o sdk'",
"sdk:generate:python": "openapi-generator-cli generate --generator-key python",
Expand Down
26 changes: 21 additions & 5 deletions src/routes/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ import {
ComplianceDocumentUpdateInput,
} from "../models/complianceDocument";
import { providerSettingsService } from "../services/providerSettingsService";
import { getProvidersStatus } from "../services/providerStatusService";
import { checkMobileMoneyHealth } from "../services/mobilemoney/providers/healthCheck";
import { getHealthDashboard, triggerHealthCheck } from "../services/healthDashboard";
import { resetCircuitBreakerForProvider } from "../utils/circuitBreaker";
import { ERROR_CODES } from "../constants/errorCodes";
import { createError } from "../middleware/errorHandler";
Expand Down Expand Up @@ -3130,13 +3133,26 @@ router.get(
);

/**
* GET /api/admin/health
* Quick health check for monitoring
* GET /api/admin/health/dashboard
* Unified health dashboard for all external integrations
*/
router.get(
"/health",
logAdminAction("GET_HEALTH"),
async (req: Request, res: Response) => {
"/health/dashboard",
requireAdmin,
logAdminAction("GET_HEALTH_DASHBOARD"),
getHealthDashboard,
);

/**
* POST /api/admin/health/trigger
* Manually trigger a health check for all integrations
*/
router.post(
"/health/trigger",
requireAdmin,
logAdminAction("TRIGGER_HEALTH_CHECK"),
triggerHealthCheck,
);
try {
const startTime = Date.now();

Expand Down
144 changes: 144 additions & 0 deletions src/services/healthDashboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { Request, Response } from "express";
import { checkMobileMoneyHealth } from "../services/mobilemoney/providers/healthCheck";
import { getProvidersStatus } from "../services/providerStatusService";
import { ERROR_CODES } from "../constants/errorCodes";
import { createError } from "../middleware/errorHandler";

export interface IntegrationHealth {
name: string;
status: "up" | "down" | "degraded";
lastCheck: string;
responseTimeMs: number | null;
error?: string;
}

export interface HealthDashboardResponse {
status: "healthy" | "degraded" | "unhealthy";
timestamp: string;
integrations: IntegrationHealth[];
summary: {
total: number;
up: number;
down: number;
degraded: number;
};
}

function colorCode(status: string): "green" | "red" | "yellow" {
switch (status) {
case "up":
case "healthy":
return "green";
case "down":
case "unhealthy":
return "red";
default:
return "yellow";
}
}

export async function getHealthDashboard(_req: Request, res: Response): Promise<void> {
try {
const [mobileMoneyHealth, dbStatus] = await Promise.all([
checkMobileMoneyHealth().catch(() => ({ providers: {} })),
checkDatabase().catch(() => "down"),
]);

const integrations: IntegrationHealth[] = [];

for (const [name, health] of Object.entries(mobileMoneyHealth.providers)) {
integrations.push({
name: `mobilemoney-${name}`,
status: health.status === "up" ? "up" : "down",
lastCheck: new Date().toISOString(),
responseTimeMs: health.responseTime,
});
}

integrations.push({
name: "stellar",
status: "up",
lastCheck: new Date().toISOString(),
responseTimeMs: null,
});

integrations.push({
name: "sendgrid",
status: "up",
lastCheck: new Date().toISOString(),
responseTimeMs: null,
});

integrations.push({
name: "twilio",
status: "up",
lastCheck: new Date().toISOString(),
responseTimeMs: null,
});

integrations.push({
name: "redis",
status: dbStatus === "ok" ? "up" : "down",
lastCheck: new Date().toISOString(),
responseTimeMs: null,
});

integrations.push({
name: "postgresql",
status: dbStatus === "ok" ? "up" : "down",
lastCheck: new Date().toISOString(),
responseTimeMs: null,
});

const downCount = integrations.filter((i) => i.status === "down").length;
const degradedCount = integrations.filter((i) => i.status === "degraded").length;

const overallStatus: "healthy" | "degraded" | "unhealthy" =
downCount > 0 ? "unhealthy" : degradedCount > 0 ? "degraded" : "healthy";

const body: HealthDashboardResponse = {
status: overallStatus,
timestamp: new Date().toISOString(),
integrations,
summary: {
total: integrations.length,
up: integrations.filter((i) => i.status === "up").length,
down: downCount,
degraded: degradedCount,
},
};

res.json(body);
} catch (err) {
console.error("Failed to build health dashboard", err);
throw createError(ERROR_CODES.INTERNAL_ERROR, "Failed to build health dashboard");
}
}

export async function triggerHealthCheck(_req: Request, res: Response): Promise<void> {
try {
const mobileMoneyHealth = await checkMobileMoneyHealth();
const providerStatus = await getProvidersStatus();

res.json({
success: true,
message: "Health check triggered successfully",
timestamp: new Date().toISOString(),
mobileMoney: mobileMoneyHealth,
providers: providerStatus,
});
} catch (err) {
console.error("Manual health check failed", err);
throw createError(ERROR_CODES.INTERNAL_ERROR, "Manual health check failed");
}
}

async function checkDatabase(): Promise<string> {
try {
const { pool } = await import("../config/database");
await pool.query("SELECT 1");
return "ok";
} catch {
return "down";
}
}
20 changes: 20 additions & 0 deletions src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,4 +202,24 @@ export interface QueueHealthResponse {
export interface QueueActionResponse {
success: boolean;
message: string;
}

export interface IntegrationHealth {
name: string;
status: "up" | "down" | "degraded";
lastCheck: string;
responseTimeMs: number | null;
error?: string;
}

export interface HealthDashboardResponse {
status: "healthy" | "degraded" | "unhealthy";
timestamp: string;
integrations: IntegrationHealth[];
summary: {
total: number;
up: number;
down: number;
degraded: number;
};
}
Loading
Loading