feat: rebrand Hemmelig to paste.es for cloudhost.es
- 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>
This commit is contained in:
159
api/routes/metrics.ts
Normal file
159
api/routes/metrics.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { timingSafeEqual } from 'crypto';
|
||||
import { Hono } from 'hono';
|
||||
import { collectDefaultMetrics, Gauge, Histogram, register, Registry } from 'prom-client';
|
||||
import config from '../config';
|
||||
import prisma from '../lib/db';
|
||||
import { getInstanceSettings } from '../lib/settings';
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
// Create a custom registry
|
||||
const metricsRegistry = new Registry();
|
||||
|
||||
// Collect default Node.js metrics (memory, CPU, event loop, etc.)
|
||||
collectDefaultMetrics({ register: metricsRegistry });
|
||||
|
||||
// Custom application metrics
|
||||
const activeSecretsGauge = new Gauge({
|
||||
name: 'hemmelig_secrets_active_count',
|
||||
help: 'Current number of active (unexpired) secrets',
|
||||
registers: [metricsRegistry],
|
||||
});
|
||||
|
||||
const totalUsersGauge = new Gauge({
|
||||
name: 'hemmelig_users_total',
|
||||
help: 'Total number of registered users',
|
||||
registers: [metricsRegistry],
|
||||
});
|
||||
|
||||
const visitorsUnique30dGauge = new Gauge({
|
||||
name: 'hemmelig_visitors_unique_30d',
|
||||
help: 'Unique visitors in the last 30 days',
|
||||
registers: [metricsRegistry],
|
||||
});
|
||||
|
||||
const visitorsViews30dGauge = new Gauge({
|
||||
name: 'hemmelig_visitors_views_30d',
|
||||
help: 'Total page views in the last 30 days',
|
||||
registers: [metricsRegistry],
|
||||
});
|
||||
|
||||
const httpRequestDuration = new Histogram({
|
||||
name: 'hemmelig_http_request_duration_seconds',
|
||||
help: 'Duration of HTTP requests in seconds',
|
||||
labelNames: ['method', 'route', 'status_code'],
|
||||
buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5],
|
||||
registers: [metricsRegistry],
|
||||
});
|
||||
|
||||
// Function to update gauge metrics from database
|
||||
async function updateGaugeMetrics() {
|
||||
try {
|
||||
const now = new Date();
|
||||
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||
|
||||
// Count active secrets (not expired)
|
||||
const activeSecrets = await prisma.secrets.count({
|
||||
where: {
|
||||
expiresAt: {
|
||||
gt: now,
|
||||
},
|
||||
},
|
||||
});
|
||||
activeSecretsGauge.set(activeSecrets);
|
||||
|
||||
// Count total users
|
||||
const totalUsers = await prisma.user.count();
|
||||
totalUsersGauge.set(totalUsers);
|
||||
|
||||
// Get visitor stats for the last 30 days
|
||||
const visitorStats = await prisma.$queryRaw<
|
||||
Array<{ unique_visitors: bigint; total_views: bigint }>
|
||||
>`
|
||||
SELECT
|
||||
COUNT(DISTINCT uniqueId) as unique_visitors,
|
||||
COUNT(*) as total_views
|
||||
FROM visitor_analytics
|
||||
WHERE timestamp >= ${thirtyDaysAgo}
|
||||
`;
|
||||
|
||||
if (visitorStats.length > 0) {
|
||||
visitorsUnique30dGauge.set(Number(visitorStats[0].unique_visitors));
|
||||
visitorsViews30dGauge.set(Number(visitorStats[0].total_views));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update metrics gauges:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to verify Bearer token using constant-time comparison
|
||||
function verifyBearerToken(authHeader: string | undefined, expectedSecret: string): boolean {
|
||||
if (!authHeader || !expectedSecret) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parts = authHeader.split(' ');
|
||||
if (parts.length !== 2 || parts[0] !== 'Bearer') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const provided = Buffer.from(parts[1]);
|
||||
const expected = Buffer.from(expectedSecret);
|
||||
|
||||
// Pad to same length to prevent timing leaks on token length
|
||||
const maxLen = Math.max(provided.length, expected.length);
|
||||
const paddedProvided = Buffer.alloc(maxLen);
|
||||
const paddedExpected = Buffer.alloc(maxLen);
|
||||
provided.copy(paddedProvided);
|
||||
expected.copy(paddedExpected);
|
||||
|
||||
// Constant-time comparison to prevent timing attacks
|
||||
return timingSafeEqual(paddedProvided, paddedExpected) && provided.length === expected.length;
|
||||
}
|
||||
|
||||
// GET /api/metrics - Prometheus metrics endpoint
|
||||
app.get('/', async (c) => {
|
||||
try {
|
||||
// In managed mode, use environment-based settings; otherwise use database
|
||||
const settings = config.isManaged()
|
||||
? config.getManagedSettings()
|
||||
: await getInstanceSettings();
|
||||
|
||||
// Check if metrics are enabled
|
||||
if (!settings?.metricsEnabled) {
|
||||
return c.json({ error: 'Metrics endpoint is disabled' }, 404);
|
||||
}
|
||||
|
||||
// Verify authentication if secret is configured
|
||||
if (settings.metricsSecret) {
|
||||
const authHeader = c.req.header('Authorization');
|
||||
if (!verifyBearerToken(authHeader, settings.metricsSecret)) {
|
||||
return c.json({ error: 'Unauthorized' }, 401);
|
||||
}
|
||||
}
|
||||
|
||||
// Update gauge metrics before returning
|
||||
await updateGaugeMetrics();
|
||||
|
||||
// Get metrics in Prometheus format
|
||||
const metrics = await metricsRegistry.metrics();
|
||||
|
||||
return c.text(metrics, 200, {
|
||||
'Content-Type': register.contentType,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to generate metrics:', error);
|
||||
return c.json({ error: 'Failed to generate metrics' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
export function observeHttpRequest(
|
||||
method: string,
|
||||
route: string,
|
||||
statusCode: number,
|
||||
duration: number
|
||||
) {
|
||||
httpRequestDuration.labels(method, route, String(statusCode)).observe(duration);
|
||||
}
|
||||
|
||||
export default app;
|
||||
Reference in New Issue
Block a user