managarten/services/mana-core-auth/src/analytics/analytics.controller.ts
Till-JS 9dfad0128a 📈 feat(monitoring): upgrade to VictoriaMetrics + DuckDB analytics
- Replace Prometheus with VictoriaMetrics (2-year retention)
- Add DuckDB analytics module for business KPIs (unlimited retention)
- Add master overview dashboard combining all metrics
- Add business metrics dashboard for user growth tracking
- Add backup script for VictoriaMetrics snapshots and DuckDB
- Add ADR documentation for monitoring stack decision

Analytics API endpoints:
- GET /api/v1/analytics/health - Service health
- GET /api/v1/analytics/latest - Latest metrics snapshot
- GET /api/v1/analytics/growth - User growth over time
- GET /api/v1/analytics/monthly - Monthly aggregates
- POST /api/v1/analytics/snapshot - Manual snapshot trigger
2026-01-28 12:38:04 +01:00

135 lines
3.4 KiB
TypeScript

import { Controller, Get, Post, Query, Res, HttpStatus } from '@nestjs/common';
import { Response } from 'express';
import { AnalyticsService } from './analytics.service';
@Controller('analytics')
export class AnalyticsController {
constructor(private readonly analyticsService: AnalyticsService) {}
/**
* Health check endpoint
*/
@Get('health')
async getHealth() {
return this.analyticsService.getHealth();
}
/**
* Get latest metrics snapshot
*/
@Get('latest')
async getLatest() {
const metrics = await this.analyticsService.getLatestMetrics();
if (!metrics) {
return { message: 'No metrics recorded yet' };
}
return metrics;
}
/**
* Get user growth data
* @param days Number of days to look back (default: 90)
*/
@Get('growth')
async getGrowth(@Query('days') days?: string) {
const numDays = days ? parseInt(days, 10) : 90;
return this.analyticsService.getUserGrowth(numDays);
}
/**
* Get monthly aggregated metrics
* @param months Number of months to look back (default: 12)
*/
@Get('monthly')
async getMonthly(@Query('months') months?: string) {
const numMonths = months ? parseInt(months, 10) : 12;
return this.analyticsService.getMonthlyMetrics(numMonths);
}
/**
* Get metrics for a date range
* @param start Start date (YYYY-MM-DD)
* @param end End date (YYYY-MM-DD)
*/
@Get('range')
async getRange(@Query('start') start: string, @Query('end') end: string) {
if (!start || !end) {
return { error: 'Both start and end dates are required (YYYY-MM-DD format)' };
}
return this.analyticsService.getMetricsRange(start, end);
}
/**
* Trigger manual snapshot (for testing/recovery)
*/
@Post('snapshot')
async triggerSnapshot() {
await this.analyticsService.recordDailySnapshot();
return { message: 'Snapshot recorded successfully' };
}
/**
* Grafana JSON API compatible endpoint - query
* Used by Grafana Infinity datasource
*/
@Post('grafana/query')
async grafanaQuery(@Res() res: Response) {
// Return available targets
const latest = await this.analyticsService.getLatestMetrics();
const growth = await this.analyticsService.getUserGrowth(30);
res.status(HttpStatus.OK).json([
{
target: 'total_users',
datapoints: growth.map((g) => [g.total_users, new Date(g.date).getTime()]),
},
{
target: 'daily_growth',
datapoints: growth.map((g) => [g.growth ?? 0, new Date(g.date).getTime()]),
},
]);
}
/**
* Grafana JSON API compatible endpoint - search
* Returns available metrics
*/
@Post('grafana/search')
async grafanaSearch() {
return [
'total_users',
'verified_users',
'new_users_today',
'new_users_week',
'new_users_month',
'daily_growth',
];
}
/**
* Summary endpoint for dashboards
*/
@Get('summary')
async getSummary() {
const latest = await this.analyticsService.getLatestMetrics();
const monthly = await this.analyticsService.getMonthlyMetrics(2);
const health = await this.analyticsService.getHealth();
const currentMonth = monthly[monthly.length - 1];
const previousMonth = monthly[monthly.length - 2];
return {
current: latest,
trends: {
month_over_month_growth:
currentMonth && previousMonth
? ((currentMonth.total_users_eom - previousMonth.total_users_eom) /
previousMonth.total_users_eom) *
100
: null,
new_users_this_month: currentMonth?.new_users ?? 0,
},
health,
};
}
}