diff --git a/app/backend/src/routes/metrics.ts b/app/backend/src/routes/metrics.ts new file mode 100644 index 0000000..d148601 --- /dev/null +++ b/app/backend/src/routes/metrics.ts @@ -0,0 +1,63 @@ +import type { FastifyInstance } from 'fastify'; +import { redis } from '../redis.js'; +import { prisma } from '../db.js'; +import { requireAuth } from '../plugins/session.js'; + +export default async function metricsRoutes(app: FastifyInstance) { + app.get('/api/v1/metrics', { preHandler: requireAuth(['admin']) }, async (_req, reply) => { + reply.header('Content-Type', 'text/plain; version=0.0.4; charset=utf-8'); + + const lines: string[] = []; + const now = Date.now(); + const uptimeSec = process.uptime(); + + lines.push(`# HELP teknav_uptime_seconds Process uptime in seconds`); + lines.push(`# TYPE teknav_uptime_seconds gauge`); + lines.push(`teknav_uptime_seconds ${uptimeSec.toFixed(2)}`); + + lines.push(`# HELP teknav_nodejs_heap_used_bytes Node.js heap used`); + lines.push(`# TYPE teknav_nodejs_heap_used_bytes gauge`); + const mem = process.memoryUsage(); + lines.push(`teknav_nodejs_heap_used_bytes ${mem.heapUsed}`); + lines.push(`# HELP teknav_nodejs_heap_total_bytes Node.js heap total`); + lines.push(`# TYPE teknav_nodejs_heap_total_bytes gauge`); + lines.push(`teknav_nodejs_heap_total_bytes ${mem.heapTotal}`); + lines.push(`# HELP teknav_nodejs_rss_bytes Node.js RSS`); + lines.push(`# TYPE teknav_nodejs_rss_bytes gauge`); + lines.push(`teknav_nodejs_rss_bytes ${mem.rss}`); + + try { + const articleCount = await prisma.article.count({ where: { status: 'منتشرشده' } }); + lines.push(`# HELP teknav_articles_published_total Total published articles`); + lines.push(`# TYPE teknav_articles_published_total gauge`); + lines.push(`teknav_articles_published_total ${articleCount}`); + + const userCount = await prisma.user.count(); + lines.push(`# HELP teknav_users_total Total users`); + lines.push(`# TYPE teknav_users_total gauge`); + lines.push(`teknav_users_total ${userCount}`); + + const subscriberCount = await prisma.newsletterSubscriber.count(); + lines.push(`# HELP teknav_newsletter_subscribers_total Newsletter subscribers`); + lines.push(`# TYPE teknav_newsletter_subscribers_total gauge`); + lines.push(`teknav_newsletter_subscribers_total ${subscriberCount}`); + } catch { + // DB unreachable — skip DB metrics + } + + try { + const activeWindow = 5 * 60 * 1000; + const cutoff = now - activeWindow; + const guests = await redis.zremrangebyscore('visitors:guests', 0, cutoff).then(() => redis.zcard('visitors:guests')); + const users = await redis.zremrangebyscore('visitors:users', 0, cutoff).then(() => redis.zcard('visitors:users')); + lines.push(`# HELP teknav_active_visitors Current active visitors`); + lines.push(`# TYPE teknav_active_visitors gauge`); + lines.push(`teknav_active_visitors{type="guest"} ${guests}`); + lines.push(`teknav_active_visitors{type="user"} ${users}`); + } catch { + // Redis unreachable — skip + } + + return reply.send(lines.join('\n') + '\n'); + }); +}