import { NextResponse } from 'next/server' import { prisma } from '@/lib/prisma' import { redis } from '@/lib/redis' export const dynamic = 'force-dynamic' export async function GET(req: Request) { // Secure endpoint with bearer token (METRICS_TOKEN env var) const metricsToken = process.env.METRICS_TOKEN if (metricsToken) { const authHeader = req.headers.get('authorization') ?? '' const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : '' if (token !== metricsToken) { return new NextResponse('Unauthorized', { status: 401 }) } } const lines: string[] = [] const metric = (name: string, help: string, type: string, value: number | string, labels = '') => { lines.push(`# HELP ${name} ${help}`) lines.push(`# TYPE ${name} ${type}`) lines.push(labels ? `${name}{${labels}} ${value}` : `${name} ${value}`) } // Multiple labeled values for the same metric name const metricLabeled = (name: string, help: string, type: string, rows: Array<{ labels: string; value: number }>) => { lines.push(`# HELP ${name} ${help}`) lines.push(`# TYPE ${name} ${type}`) for (const row of rows) { lines.push(`${name}{${row.labels}} ${row.value}`) } } // ── Uptime ────────────────────────────────────────────────────────────── metric('memento_uptime_seconds', 'Application uptime in seconds', 'gauge', process.uptime().toFixed(2)) // ── Infrastructure ─────────────────────────────────────────────────────── try { const dbStart = Date.now() const [noteCount, notebookCount, userCount] = await Promise.all([ prisma.note.count(), prisma.notebook.count(), prisma.user.count(), ]) const dbLatency = Date.now() - dbStart metric('memento_db_up', 'Database connectivity (1=up, 0=down)', 'gauge', 1) metric('memento_db_latency_ms', 'Database query latency in milliseconds', 'gauge', dbLatency) metric('memento_notes_total', 'Total number of notes', 'gauge', noteCount) metric('memento_notebooks_total', 'Total number of notebooks', 'gauge', notebookCount) metric('memento_users_total', 'Total number of users', 'gauge', userCount) } catch { metric('memento_db_up', 'Database connectivity (1=up, 0=down)', 'gauge', 0) } try { const redisStart = Date.now() await redis.ping() const dbSize = await redis.dbsize() const info = await redis.info('memory') const redisLatency = Date.now() - redisStart const memMatch = info.match(/used_memory:(\d+)/) const usedMemoryBytes = memMatch ? parseInt(memMatch[1]) : 0 metric('memento_redis_up', 'Redis connectivity (1=up, 0=down)', 'gauge', 1) metric('memento_redis_latency_ms', 'Redis ping latency in milliseconds', 'gauge', redisLatency) metric('memento_redis_keys_total', 'Total number of Redis keys', 'gauge', dbSize) metric('memento_redis_memory_bytes', 'Redis used memory in bytes', 'gauge', usedMemoryBytes) } catch { metric('memento_redis_up', 'Redis connectivity (1=up, 0=down)', 'gauge', 0) } const mem = process.memoryUsage() metric('memento_process_heap_used_bytes', 'Node.js heap used in bytes', 'gauge', mem.heapUsed) metric('memento_process_heap_total_bytes', 'Node.js heap total in bytes', 'gauge', mem.heapTotal) metric('memento_process_rss_bytes', 'Node.js RSS memory in bytes', 'gauge', mem.rss) // ── Business metrics ───────────────────────────────────────────────────── try { const now = new Date() const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1) const startOfLastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1) const endOfLastMonth = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59) const last7days = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000) const last30days = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000) // ── Subscriptions par tier ── const subsByTier = await prisma.subscription.groupBy({ by: ['tier', 'status'], _count: { _all: true }, }) const subRows = subsByTier.map(r => ({ labels: `tier="${r.tier}",status="${r.status}"`, value: r._count._all, })) metricLabeled( 'memento_subscriptions_total', 'Total subscriptions by tier and status', 'gauge', subRows, ) // Totaux agrégés utiles const activeSubs = subsByTier .filter(r => r.status === 'ACTIVE') .reduce((s, r) => s + r._count._all, 0) const canceledSubs = subsByTier .filter(r => r.status === 'CANCELED') .reduce((s, r) => s + r._count._all, 0) metric('memento_subscriptions_active_total', 'Total active subscriptions (all tiers)', 'gauge', activeSubs) metric('memento_subscriptions_canceled_total', 'Total canceled subscriptions (all tiers)', 'gauge', canceledSubs) // ── Nouveaux abonnements ce mois ── const newSubsThisMonth = await prisma.subscription.count({ where: { createdAt: { gte: startOfMonth }, status: 'ACTIVE' }, }) const newSubsLastMonth = await prisma.subscription.count({ where: { createdAt: { gte: startOfLastMonth, lte: endOfLastMonth }, status: 'ACTIVE' }, }) metric('memento_subscriptions_new_this_month', 'New active subscriptions created this month', 'gauge', newSubsThisMonth) metric('memento_subscriptions_new_last_month', 'New active subscriptions created last month', 'gauge', newSubsLastMonth) // ── Désabonnements (cancelAtPeriodEnd ou canceledAt ce mois) ── const churnsThisMonth = await prisma.subscription.count({ where: { OR: [ { canceledAt: { gte: startOfMonth } }, { cancelAtPeriodEnd: true, updatedAt: { gte: startOfMonth } }, ], }, }) const churnsLastMonth = await prisma.subscription.count({ where: { OR: [ { canceledAt: { gte: startOfLastMonth, lte: endOfLastMonth } }, { cancelAtPeriodEnd: true, updatedAt: { gte: startOfLastMonth, lte: endOfLastMonth } }, ], }, }) metric('memento_churn_this_month', 'Cancellations / pending cancellations this month', 'gauge', churnsThisMonth) metric('memento_churn_last_month', 'Cancellations / pending cancellations last month', 'gauge', churnsLastMonth) // ── Utilisateurs actifs ── const activeUsers7d = await prisma.note.groupBy({ by: ['userId'], where: { updatedAt: { gte: last7days } }, }) const activeUsers30d = await prisma.note.groupBy({ by: ['userId'], where: { updatedAt: { gte: last30days } }, }) metric('memento_active_users_7d', 'Users who modified at least one note in the last 7 days', 'gauge', activeUsers7d.length) metric('memento_active_users_30d', 'Users who modified at least one note in the last 30 days', 'gauge', activeUsers30d.length) // Nouveaux utilisateurs const newUsers7d = await prisma.user.count({ where: { createdAt: { gte: last7days } } }) const newUsersThisMonth = await prisma.user.count({ where: { createdAt: { gte: startOfMonth } } }) metric('memento_new_users_7d', 'New user registrations in the last 7 days', 'gauge', newUsers7d) metric('memento_new_users_this_month', 'New user registrations this month', 'gauge', newUsersThisMonth) // ── Agents IA ── const agentsByStatus = await prisma.agentAction.groupBy({ by: ['status'], _count: { _all: true }, where: { createdAt: { gte: last30days } }, }) const agentRows = agentsByStatus.map(r => ({ labels: `status="${r.status}"`, value: r._count._all, })) metricLabeled( 'memento_agent_runs_30d', 'Agent runs by status in the last 30 days', 'gauge', agentRows, ) const agentRunsToday = await prisma.agentAction.count({ where: { createdAt: { gte: new Date(now.getFullYear(), now.getMonth(), now.getDate()) } }, }) metric('memento_agent_runs_today', 'Agent runs triggered today', 'gauge', agentRunsToday) // Tokens consommés par les agents const agentTokens = await prisma.agentAction.aggregate({ _sum: { tokensUsed: true }, where: { createdAt: { gte: startOfMonth } }, }) metric('memento_agent_tokens_this_month', 'Total tokens consumed by agents this month', 'gauge', agentTokens._sum.tokensUsed ?? 0) // ── Usage IA par feature (ce mois) ── const usageByFeature = await prisma.usageLog.groupBy({ by: ['feature'], _sum: { requestsCount: true, tokensUsed: true }, where: { periodStart: { gte: startOfMonth } }, }) const usageRequestRows = usageByFeature.map(r => ({ labels: `feature="${r.feature}"`, value: r._sum.requestsCount ?? 0, })) const usageTokenRows = usageByFeature.map(r => ({ labels: `feature="${r.feature}"`, value: r._sum.tokensUsed ?? 0, })) metricLabeled('memento_ai_requests_this_month', 'AI API requests by feature this month', 'gauge', usageRequestRows) metricLabeled('memento_ai_tokens_this_month', 'AI tokens consumed by feature this month', 'gauge', usageTokenRows) // ── Logins (AuditLog) ── const loginsToday = await prisma.auditLog.count({ where: { action: 'LOGIN', createdAt: { gte: new Date(now.getFullYear(), now.getMonth(), now.getDate()) }, }, }) const loginsThisMonth = await prisma.auditLog.count({ where: { action: 'LOGIN', createdAt: { gte: startOfMonth } }, }) metric('memento_logins_today', 'Login events today', 'gauge', loginsToday) metric('memento_logins_this_month', 'Login events this month', 'gauge', loginsThisMonth) // ── Brainstorm sessions ── const brainstormThisMonth = await prisma.brainstormSession.count({ where: { createdAt: { gte: startOfMonth } }, }) metric('memento_brainstorm_sessions_this_month', 'Brainstorm sessions created this month', 'gauge', brainstormThisMonth) // ── Flashcards ── const flashcardsTotal = await prisma.flashcard.count() const flashcardsReviewedThisMonth = await prisma.flashcardReview.count({ where: { reviewedAt: { gte: startOfMonth } }, }) metric('memento_flashcards_total', 'Total flashcards in the system', 'gauge', flashcardsTotal) metric('memento_flashcard_reviews_this_month', 'Flashcard review events this month', 'gauge', flashcardsReviewedThisMonth) } catch (err) { console.error('[metrics] Business metrics error:', err) metric('memento_business_metrics_error', 'Business metrics collection failed (1=error)', 'gauge', 1) } const body = lines.join('\n') + '\n' return new NextResponse(body, { status: 200, headers: { 'Content-Type': 'text/plain; version=0.0.4; charset=utf-8', }, }) }