- Set Spanish as default language with ephemeral/encrypted privacy focus - Translate all user-facing strings and legal pages to Spanish - Replace Norwegian flag with Spanish flag in footer - Remove Hemmelig/terces.cloud links, add cloudhost.es sponsorship - Rewrite PrivacyPage: zero data collection, ephemeral design emphasis - Rewrite TermsPage: Spanish law, RGPD, paste.es/CloudHost.es references - Update PWA manifest, HTML meta tags, package.json branding - Rename webhook headers to X-Paste-Event / X-Paste-Signature - Update API docs title and contact to paste.es / cloudhost.es Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
132 lines
3.5 KiB
TypeScript
132 lines
3.5 KiB
TypeScript
import { constants } from 'fs';
|
|
import { access, unlink, writeFile } from 'fs/promises';
|
|
import { Hono } from 'hono';
|
|
import { join } from 'path';
|
|
import prisma from '../lib/db';
|
|
import { UPLOAD_DIR } from '../lib/files';
|
|
|
|
const app = new Hono();
|
|
|
|
type CheckStatus = 'healthy' | 'unhealthy';
|
|
|
|
type CheckResult = {
|
|
status: CheckStatus;
|
|
latency_ms?: number;
|
|
error?: string;
|
|
[key: string]: unknown;
|
|
};
|
|
|
|
type HealthResponse = {
|
|
status: CheckStatus;
|
|
timestamp: string;
|
|
checks: {
|
|
database: CheckResult;
|
|
storage: CheckResult;
|
|
memory: CheckResult;
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Check database connectivity by executing a simple query
|
|
*/
|
|
async function checkDatabase(): Promise<CheckResult> {
|
|
const start = Date.now();
|
|
try {
|
|
await prisma.$queryRaw`SELECT 1`;
|
|
return {
|
|
status: 'healthy',
|
|
latency_ms: Date.now() - start,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
status: 'unhealthy',
|
|
latency_ms: Date.now() - start,
|
|
error: error instanceof Error ? error.message : 'Database connection failed',
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check file storage is accessible and writable
|
|
*/
|
|
async function checkStorage(): Promise<CheckResult> {
|
|
const testFile = join(UPLOAD_DIR, `.health-check-${Date.now()}`);
|
|
try {
|
|
// Check directory exists and is accessible
|
|
await access(UPLOAD_DIR, constants.R_OK | constants.W_OK);
|
|
|
|
// Try to write and delete a test file
|
|
await writeFile(testFile, 'health-check');
|
|
await unlink(testFile);
|
|
|
|
return {
|
|
status: 'healthy',
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
status: 'unhealthy',
|
|
error: error instanceof Error ? error.message : 'Storage check failed',
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check memory usage is within acceptable bounds
|
|
* Note: heapUsed/heapTotal ratio is often high (90%+) in normal Node.js operation
|
|
* since the heap grows dynamically. We use RSS-based threshold instead.
|
|
*/
|
|
function checkMemory(): CheckResult {
|
|
const memUsage = process.memoryUsage();
|
|
const heapUsedMB = Math.round(memUsage.heapUsed / 1024 / 1024);
|
|
const heapTotalMB = Math.round(memUsage.heapTotal / 1024 / 1024);
|
|
const rssMB = Math.round(memUsage.rss / 1024 / 1024);
|
|
|
|
// Consider unhealthy if RSS exceeds 1GB (reasonable default for most deployments)
|
|
const RSS_THRESHOLD_MB = 1024;
|
|
const isHealthy = rssMB < RSS_THRESHOLD_MB;
|
|
|
|
return {
|
|
status: isHealthy ? 'healthy' : 'unhealthy',
|
|
heap_used_mb: heapUsedMB,
|
|
heap_total_mb: heapTotalMB,
|
|
rss_mb: rssMB,
|
|
rss_threshold_mb: RSS_THRESHOLD_MB,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* GET /health/live - Liveness probe
|
|
* Simple check to verify the process is running
|
|
*/
|
|
app.get('/live', (c) => {
|
|
return c.json({ status: 'healthy', timestamp: new Date().toISOString() });
|
|
});
|
|
|
|
/**
|
|
* GET /health/ready - Readiness probe
|
|
* Comprehensive check of all dependencies
|
|
*/
|
|
app.get('/ready', async (c) => {
|
|
const [database, storage] = await Promise.all([checkDatabase(), checkStorage()]);
|
|
|
|
const memory = checkMemory();
|
|
|
|
const checks = { database, storage, memory };
|
|
|
|
const overallStatus: CheckStatus = Object.values(checks).every(
|
|
(check) => check.status === 'healthy'
|
|
)
|
|
? 'healthy'
|
|
: 'unhealthy';
|
|
|
|
const response: HealthResponse = {
|
|
status: overallStatus,
|
|
timestamp: new Date().toISOString(),
|
|
checks,
|
|
};
|
|
|
|
return c.json(response, overallStatus === 'healthy' ? 200 : 503);
|
|
});
|
|
|
|
export default app;
|