mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:41:09 +02:00
feat(stats-bot): add infrastructure monitoring commands
Add 5 new commands powered by Prometheus/VictoriaMetrics: - !system: Mac Mini status (CPU, RAM, Disk, Uptime, Load) - !services: Backend service health (UP/DOWN) - !traffic: HTTP traffic & latency per service - !db: PostgreSQL & Redis status - !growth: User growth statistics New modules: - PrometheusService: Query Prometheus/VictoriaMetrics API - InfrastructureService: Generate infrastructure reports Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e88597cd20
commit
bd7f19718c
8 changed files with 447 additions and 49 deletions
|
|
@ -8,7 +8,7 @@ Matrix Stats Bot delivers analytics from Umami (self-hosted) via Matrix. GDPR-co
|
|||
|
||||
- **Framework**: NestJS 10
|
||||
- **Matrix**: matrix-bot-sdk
|
||||
- **Analytics**: Umami API
|
||||
- **Analytics**: Umami API + Prometheus/VictoriaMetrics
|
||||
- **Scheduling**: @nestjs/schedule
|
||||
|
||||
## Commands
|
||||
|
|
@ -22,13 +22,30 @@ pnpm type-check # TypeScript check
|
|||
|
||||
## Matrix Commands
|
||||
|
||||
### Analytics (Umami)
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `!stats` | Overview of all apps (30 days) |
|
||||
| `!today` | Today's statistics |
|
||||
| `!week` | This week's statistics |
|
||||
| `!realtime` | Active visitors right now |
|
||||
| `!users` | Registered user statistics |
|
||||
|
||||
### Infrastructure (Prometheus)
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `!system` | Mac Mini status (CPU, RAM, Disk, Uptime) |
|
||||
| `!services` | Backend service health (UP/DOWN) |
|
||||
| `!traffic` | HTTP traffic & latency per service |
|
||||
| `!db` | PostgreSQL & Redis status |
|
||||
| `!growth` | User growth statistics |
|
||||
|
||||
### General
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `!status` | Account status |
|
||||
| `!help` | Show available commands |
|
||||
|
||||
## Scheduled Reports
|
||||
|
|
@ -54,6 +71,9 @@ UMAMI_API_URL=http://umami:3000
|
|||
UMAMI_USERNAME=admin
|
||||
UMAMI_PASSWORD=xxx
|
||||
|
||||
# Prometheus / VictoriaMetrics
|
||||
PROMETHEUS_URL=http://victoriametrics:8428
|
||||
|
||||
# Database (for user counts)
|
||||
DATABASE_URL=postgresql://...
|
||||
```
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@ import { Module } from '@nestjs/common';
|
|||
import { MatrixService } from './matrix.service';
|
||||
import { AnalyticsModule } from '../analytics/analytics.module';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
import { InfrastructureModule } from '../infrastructure/infrastructure.module';
|
||||
import { TranscriptionModule, SessionModule, CreditModule } from '@manacore/bot-services';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
AnalyticsModule,
|
||||
UsersModule,
|
||||
InfrastructureModule,
|
||||
TranscriptionModule.register({
|
||||
sttUrl: process.env.STT_URL || 'http://localhost:3020',
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
} from '@manacore/matrix-bot-common';
|
||||
import { AnalyticsService } from '../analytics/analytics.service';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { InfrastructureService } from '../infrastructure/infrastructure.service';
|
||||
import { TranscriptionService, SessionService, CreditService } from '@manacore/bot-services';
|
||||
|
||||
@Injectable()
|
||||
|
|
@ -22,12 +23,18 @@ export class MatrixService extends BaseMatrixService {
|
|||
{ keywords: ['woche', 'week', 'wochenstatistik'], command: 'week' },
|
||||
{ keywords: ['realtime', 'live', 'aktive', 'jetzt'], command: 'realtime' },
|
||||
{ keywords: ['users', 'benutzer', 'nutzer', 'registrierte'], command: 'users' },
|
||||
{ keywords: ['system', 'server', 'macmini', 'mac'], command: 'system' },
|
||||
{ keywords: ['services', 'dienste', 'backends', 'health'], command: 'services' },
|
||||
{ keywords: ['traffic', 'requests', 'http', 'api'], command: 'traffic' },
|
||||
{ keywords: ['db', 'database', 'datenbank', 'postgres', 'redis'], command: 'db' },
|
||||
{ keywords: ['growth', 'wachstum', 'registrierungen'], command: 'growth' },
|
||||
]);
|
||||
|
||||
constructor(
|
||||
configService: ConfigService,
|
||||
private analyticsService: AnalyticsService,
|
||||
private usersService: UsersService,
|
||||
private infrastructureService: InfrastructureService,
|
||||
private readonly transcriptionService: TranscriptionService,
|
||||
private sessionService: SessionService,
|
||||
private creditService: CreditService
|
||||
|
|
@ -95,14 +102,6 @@ export class MatrixService extends BaseMatrixService {
|
|||
await this.sendHelp(roomId);
|
||||
break;
|
||||
|
||||
case 'login':
|
||||
await this.handleLogin(roomId, sender, args);
|
||||
break;
|
||||
|
||||
case 'logout':
|
||||
await this.handleLogout(roomId, sender);
|
||||
break;
|
||||
|
||||
case 'status':
|
||||
await this.handleStatus(roomId, sender);
|
||||
break;
|
||||
|
|
@ -127,28 +126,50 @@ export class MatrixService extends BaseMatrixService {
|
|||
await this.sendUsers(roomId);
|
||||
break;
|
||||
|
||||
case 'system':
|
||||
await this.sendSystem(roomId);
|
||||
break;
|
||||
|
||||
case 'services':
|
||||
await this.sendServices(roomId);
|
||||
break;
|
||||
|
||||
case 'traffic':
|
||||
await this.sendTraffic(roomId);
|
||||
break;
|
||||
|
||||
case 'db':
|
||||
await this.sendDatabase(roomId);
|
||||
break;
|
||||
|
||||
case 'growth':
|
||||
await this.sendGrowth(roomId);
|
||||
break;
|
||||
|
||||
default:
|
||||
await this.sendMessage(roomId, `Unbekannter Befehl: !${command}\n\nVerwende !help`);
|
||||
}
|
||||
}
|
||||
|
||||
private async sendHelp(roomId: string) {
|
||||
const helpText = `**📊 ManaCore Stats Bot (DSGVO-konform)**
|
||||
const helpText = `**📊 ManaCore Stats Bot**
|
||||
|
||||
**Account:**
|
||||
- \`!login email passwort\` - Anmelden
|
||||
- \`!logout\` - Abmelden
|
||||
- \`!status\` - Account Status
|
||||
|
||||
**Statistiken:**
|
||||
**Analytics (Umami):**
|
||||
- \`!stats\` - Übersicht aller Apps (30 Tage)
|
||||
- \`!today\` - Heutige Statistiken
|
||||
- \`!week\` - Wochenstatistiken
|
||||
- \`!realtime\` - Aktive Besucher jetzt
|
||||
- \`!users\` - Registrierte Benutzer
|
||||
- \`!help\` - Diese Hilfe
|
||||
|
||||
Daten von Umami Analytics (self-hosted).`;
|
||||
**Infrastruktur (Prometheus):**
|
||||
- \`!system\` - Mac Mini Status (CPU, RAM, Disk)
|
||||
- \`!services\` - Backend Service Status
|
||||
- \`!traffic\` - HTTP Traffic & Latenz
|
||||
- \`!db\` - Datenbank Status
|
||||
- \`!growth\` - User Wachstum
|
||||
|
||||
**Account:**
|
||||
- \`!status\` - Account Status
|
||||
- \`!help\` - Diese Hilfe`;
|
||||
|
||||
await this.sendMessage(roomId, helpText);
|
||||
}
|
||||
|
|
@ -160,7 +181,10 @@ Daten von Umami Analytics (self-hosted).`;
|
|||
await this.sendMessage(roomId, report);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to generate stats overview:', error);
|
||||
await this.sendMessage(roomId, `❌ Fehler beim Laden der Statistiken: ${error instanceof Error ? error.message : String(error)}`);
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`❌ Fehler beim Laden der Statistiken: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -171,7 +195,10 @@ Daten von Umami Analytics (self-hosted).`;
|
|||
await this.sendMessage(roomId, report);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to generate daily report:', error);
|
||||
await this.sendMessage(roomId, `❌ Fehler beim Laden: ${error instanceof Error ? error.message : String(error)}`);
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`❌ Fehler beim Laden: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -182,7 +209,10 @@ Daten von Umami Analytics (self-hosted).`;
|
|||
await this.sendMessage(roomId, report);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to generate weekly report:', error);
|
||||
await this.sendMessage(roomId, `❌ Fehler beim Laden: ${error instanceof Error ? error.message : String(error)}`);
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`❌ Fehler beim Laden: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -192,7 +222,10 @@ Daten von Umami Analytics (self-hosted).`;
|
|||
await this.sendMessage(roomId, report);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to generate realtime report:', error);
|
||||
await this.sendMessage(roomId, `❌ Fehler beim Laden: ${error instanceof Error ? error.message : String(error)}`);
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`❌ Fehler beim Laden: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -216,34 +249,69 @@ Daten von Umami Analytics (self-hosted).`;
|
|||
await this.sendMessage(roomId, report);
|
||||
}
|
||||
|
||||
private async handleLogin(roomId: string, sender: string, args: string) {
|
||||
const parts = args.split(' ');
|
||||
if (parts.length < 2 || !parts[0] || !parts[1]) {
|
||||
await this.sendMessage(roomId, 'Verwendung: `!login email passwort`');
|
||||
return;
|
||||
}
|
||||
const [email, password] = parts;
|
||||
const result = await this.sessionService.login(sender, email, password);
|
||||
|
||||
if (result.success) {
|
||||
const token = await this.sessionService.getToken(sender);
|
||||
if (token) {
|
||||
const balance = await this.creditService.getBalance(token);
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`✅ Erfolgreich angemeldet als **${email}**\n⚡ Credits: ${balance.balance.toFixed(2)}`
|
||||
);
|
||||
} else {
|
||||
await this.sendMessage(roomId, `✅ Erfolgreich angemeldet als **${email}**`);
|
||||
}
|
||||
} else {
|
||||
await this.sendMessage(roomId, `❌ Anmeldung fehlgeschlagen: ${result.error}`);
|
||||
private async sendSystem(roomId: string) {
|
||||
try {
|
||||
const report = await this.infrastructureService.generateSystemReport();
|
||||
await this.sendMessage(roomId, report);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to generate system report:', error);
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`❌ Fehler: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleLogout(roomId: string, sender: string) {
|
||||
await this.sessionService.logout(sender);
|
||||
await this.sendMessage(roomId, '👋 Erfolgreich abgemeldet.');
|
||||
private async sendServices(roomId: string) {
|
||||
try {
|
||||
const report = await this.infrastructureService.generateServicesReport();
|
||||
await this.sendMessage(roomId, report);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to generate services report:', error);
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`❌ Fehler: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async sendTraffic(roomId: string) {
|
||||
try {
|
||||
const report = await this.infrastructureService.generateTrafficReport();
|
||||
await this.sendMessage(roomId, report);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to generate traffic report:', error);
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`❌ Fehler: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async sendDatabase(roomId: string) {
|
||||
try {
|
||||
const report = await this.infrastructureService.generateDatabaseReport();
|
||||
await this.sendMessage(roomId, report);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to generate database report:', error);
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`❌ Fehler: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async sendGrowth(roomId: string) {
|
||||
try {
|
||||
const report = await this.infrastructureService.generateGrowthReport();
|
||||
await this.sendMessage(roomId, report);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to generate growth report:', error);
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`❌ Fehler: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleStatus(roomId: string, sender: string) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ export default () => ({
|
|||
database: {
|
||||
url: process.env.DATABASE_URL || '',
|
||||
},
|
||||
prometheus: {
|
||||
url: process.env.PROMETHEUS_URL || 'http://localhost:9090',
|
||||
},
|
||||
});
|
||||
|
||||
// Website IDs from Umami - update these with actual UUIDs
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { PrometheusModule } from '../prometheus/prometheus.module';
|
||||
import { InfrastructureService } from './infrastructure.service';
|
||||
|
||||
@Module({
|
||||
imports: [PrometheusModule],
|
||||
providers: [InfrastructureService],
|
||||
exports: [InfrastructureService],
|
||||
})
|
||||
export class InfrastructureModule {}
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { PrometheusService } from '../prometheus/prometheus.service';
|
||||
|
||||
@Injectable()
|
||||
export class InfrastructureService {
|
||||
private readonly logger = new Logger(InfrastructureService.name);
|
||||
|
||||
constructor(private readonly prometheus: PrometheusService) {}
|
||||
|
||||
async generateSystemReport(): Promise<string> {
|
||||
const [cpu, memory, disk, uptime, load] = await Promise.all([
|
||||
this.prometheus.getValue('100 - (avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)'),
|
||||
this.prometheus.getValue(
|
||||
'100 * (1 - ((node_memory_free_bytes + node_memory_cached_bytes + node_memory_buffers_bytes) / node_memory_total_bytes))'
|
||||
),
|
||||
this.prometheus.getValue(
|
||||
'100 - ((node_filesystem_avail_bytes{mountpoint="/",fstype!="rootfs"} / node_filesystem_size_bytes{mountpoint="/",fstype!="rootfs"}) * 100)'
|
||||
),
|
||||
this.prometheus.getValue('time() - node_boot_time_seconds'),
|
||||
this.prometheus.getValue('node_load1'),
|
||||
]);
|
||||
|
||||
if (cpu === null && memory === null) {
|
||||
return '❌ Keine System-Metriken verfügbar. Node Exporter nicht erreichbar.';
|
||||
}
|
||||
|
||||
const formatUptime = (seconds: number): string => {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const mins = Math.floor((seconds % 3600) / 60);
|
||||
if (days > 0) return `${days}d ${hours}h`;
|
||||
if (hours > 0) return `${hours}h ${mins}m`;
|
||||
return `${mins}m`;
|
||||
};
|
||||
|
||||
const getStatusIcon = (value: number, warn: number, crit: number): string => {
|
||||
if (value >= crit) return '🔴';
|
||||
if (value >= warn) return '🟡';
|
||||
return '🟢';
|
||||
};
|
||||
|
||||
let report = '**🖥️ Mac Mini System Status**\n\n';
|
||||
report += `${getStatusIcon(cpu || 0, 70, 85)} **CPU:** ${cpu?.toFixed(1) || '?'}%\n`;
|
||||
report += `${getStatusIcon(memory || 0, 70, 85)} **Memory:** ${memory?.toFixed(1) || '?'}%\n`;
|
||||
report += `${getStatusIcon(disk || 0, 70, 85)} **Disk:** ${disk?.toFixed(1) || '?'}%\n`;
|
||||
report += `⏱️ **Uptime:** ${uptime ? formatUptime(uptime) : '?'}\n`;
|
||||
report += `📊 **Load (1m):** ${load?.toFixed(2) || '?'}`;
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
async generateServicesReport(): Promise<string> {
|
||||
const services = [
|
||||
{ job: 'mana-core-auth', name: 'Auth' },
|
||||
{ job: 'chat-backend', name: 'Chat' },
|
||||
{ job: 'todo-backend', name: 'Todo' },
|
||||
{ job: 'calendar-backend', name: 'Calendar' },
|
||||
{ job: 'clock-backend', name: 'Clock' },
|
||||
{ job: 'contacts-backend', name: 'Contacts' },
|
||||
{ job: 'zitare-backend', name: 'Zitare' },
|
||||
{ job: 'picture-backend', name: 'Picture' },
|
||||
];
|
||||
|
||||
const results = await this.prometheus.query('up');
|
||||
const statusMap = new Map<string, number>();
|
||||
for (const result of results) {
|
||||
statusMap.set(result.metric.job, parseFloat(result.value[1]));
|
||||
}
|
||||
|
||||
// Also check infrastructure
|
||||
const pgUp = await this.prometheus.getValue('pg_up');
|
||||
const redisUp = await this.prometheus.getValue('redis_up');
|
||||
|
||||
let report = '**🔧 Service Status**\n\n';
|
||||
let allUp = true;
|
||||
|
||||
for (const service of services) {
|
||||
const status = statusMap.get(service.job);
|
||||
if (status === 1) {
|
||||
report += `🟢 ${service.name}\n`;
|
||||
} else if (status === 0) {
|
||||
report += `🔴 ${service.name}\n`;
|
||||
allUp = false;
|
||||
} else {
|
||||
report += `⚪ ${service.name} (nicht konfiguriert)\n`;
|
||||
}
|
||||
}
|
||||
|
||||
report += '\n**Infrastruktur:**\n';
|
||||
report += pgUp === 1 ? '🟢 PostgreSQL\n' : '🔴 PostgreSQL\n';
|
||||
report += redisUp === 1 ? '🟢 Redis' : '🔴 Redis';
|
||||
|
||||
if (allUp && pgUp === 1 && redisUp === 1) {
|
||||
report =
|
||||
'**🔧 Service Status**\n\n✅ Alle Services online!\n\n' +
|
||||
report.split('\n\n').slice(1).join('\n\n');
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
async generateTrafficReport(): Promise<string> {
|
||||
const [requestRates, errorRates, p95Latency] = await Promise.all([
|
||||
this.prometheus.query('sum(rate(http_requests_total[5m])) by (job)'),
|
||||
this.prometheus.query('sum(rate(http_requests_total{status=~"5.."}[5m])) by (job)'),
|
||||
this.prometheus.query(
|
||||
'histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, job))'
|
||||
),
|
||||
]);
|
||||
|
||||
if (requestRates.length === 0) {
|
||||
return '❌ Keine Traffic-Metriken verfügbar.';
|
||||
}
|
||||
|
||||
const rateMap = new Map<string, number>();
|
||||
const errorMap = new Map<string, number>();
|
||||
const latencyMap = new Map<string, number>();
|
||||
|
||||
for (const r of requestRates) {
|
||||
rateMap.set(r.metric.job, parseFloat(r.value[1]));
|
||||
}
|
||||
for (const r of errorRates) {
|
||||
errorMap.set(r.metric.job, parseFloat(r.value[1]));
|
||||
}
|
||||
for (const r of p95Latency) {
|
||||
latencyMap.set(r.metric.job, parseFloat(r.value[1]));
|
||||
}
|
||||
|
||||
const totalRate = Array.from(rateMap.values()).reduce((a, b) => a + b, 0);
|
||||
const totalErrors = Array.from(errorMap.values()).reduce((a, b) => a + b, 0);
|
||||
|
||||
let report = '**📈 HTTP Traffic**\n\n';
|
||||
report += `**Gesamt:** ${totalRate.toFixed(2)} req/s\n`;
|
||||
report += `**5xx Errors:** ${totalErrors.toFixed(3)} req/s\n\n`;
|
||||
|
||||
const serviceNames: Record<string, string> = {
|
||||
'mana-core-auth': 'Auth',
|
||||
'chat-backend': 'Chat',
|
||||
'todo-backend': 'Todo',
|
||||
'calendar-backend': 'Calendar',
|
||||
'clock-backend': 'Clock',
|
||||
'contacts-backend': 'Contacts',
|
||||
};
|
||||
|
||||
for (const [job, rate] of rateMap.entries()) {
|
||||
if (rate < 0.001) continue;
|
||||
const name = serviceNames[job] || job;
|
||||
const latency = latencyMap.get(job);
|
||||
const latencyStr = latency ? `${(latency * 1000).toFixed(0)}ms` : '?';
|
||||
report += `**${name}:** ${rate.toFixed(2)} req/s (p95: ${latencyStr})\n`;
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
async generateDatabaseReport(): Promise<string> {
|
||||
const [pgConnections, dbSizes, redisMemory, redisClients] = await Promise.all([
|
||||
this.prometheus.getValue('sum(pg_stat_activity_count)'),
|
||||
this.prometheus.query('pg_database_size_bytes{datname!~"template.*|postgres"}'),
|
||||
this.prometheus.getValue('redis_memory_used_bytes'),
|
||||
this.prometheus.getValue('redis_connected_clients'),
|
||||
]);
|
||||
|
||||
if (pgConnections === null && redisMemory === null) {
|
||||
return '❌ Keine Datenbank-Metriken verfügbar.';
|
||||
}
|
||||
|
||||
const formatBytes = (bytes: number): string => {
|
||||
if (bytes >= 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
||||
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${bytes} B`;
|
||||
};
|
||||
|
||||
let report = '**🗄️ Datenbank Status**\n\n';
|
||||
report += '**PostgreSQL:**\n';
|
||||
report += `- Connections: ${pgConnections || '?'}\n`;
|
||||
|
||||
if (dbSizes.length > 0) {
|
||||
const sortedDbs = dbSizes
|
||||
.map((r) => ({ name: r.metric.datname, size: parseFloat(r.value[1]) }))
|
||||
.sort((a, b) => b.size - a.size)
|
||||
.slice(0, 5);
|
||||
|
||||
for (const db of sortedDbs) {
|
||||
report += `- ${db.name}: ${formatBytes(db.size)}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
report += '\n**Redis:**\n';
|
||||
report += `- Memory: ${redisMemory ? formatBytes(redisMemory) : '?'}\n`;
|
||||
report += `- Clients: ${redisClients || '?'}`;
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
async generateGrowthReport(): Promise<string> {
|
||||
const [total, verified, today, week, month] = await Promise.all([
|
||||
this.prometheus.getValue('auth_users_total'),
|
||||
this.prometheus.getValue('auth_users_verified'),
|
||||
this.prometheus.getValue('auth_users_created_today'),
|
||||
this.prometheus.getValue('auth_users_created_this_week'),
|
||||
this.prometheus.getValue('auth_users_created_this_month'),
|
||||
]);
|
||||
|
||||
if (total === null) {
|
||||
return '❌ Keine User-Metriken verfügbar. Auth Service nicht erreichbar.';
|
||||
}
|
||||
|
||||
const verificationRate = total && verified ? ((verified / total) * 100).toFixed(1) : '?';
|
||||
|
||||
let report = '**📈 User Growth**\n\n';
|
||||
report += `**Gesamt:** ${total?.toLocaleString() || '?'} User\n`;
|
||||
report += `**Verifiziert:** ${verified?.toLocaleString() || '?'} (${verificationRate}%)\n\n`;
|
||||
report += '**Neue Registrierungen:**\n';
|
||||
report += `- Heute: ${today || 0}\n`;
|
||||
report += `- Diese Woche: ${week || 0}\n`;
|
||||
report += `- Dieser Monat: ${month || 0}`;
|
||||
|
||||
return report;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { PrometheusService } from './prometheus.service';
|
||||
|
||||
@Module({
|
||||
providers: [PrometheusService],
|
||||
exports: [PrometheusService],
|
||||
})
|
||||
export class PrometheusModule {}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
interface PrometheusResult {
|
||||
metric: Record<string, string>;
|
||||
value: [number, string];
|
||||
}
|
||||
|
||||
interface PrometheusResponse {
|
||||
status: string;
|
||||
data: {
|
||||
resultType: string;
|
||||
result: PrometheusResult[];
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PrometheusService {
|
||||
private readonly logger = new Logger(PrometheusService.name);
|
||||
private readonly baseUrl: string;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
this.baseUrl = this.configService.get<string>('prometheus.url') || 'http://localhost:9090';
|
||||
}
|
||||
|
||||
async query(promql: string): Promise<PrometheusResult[]> {
|
||||
try {
|
||||
const url = `${this.baseUrl}/api/v1/query?query=${encodeURIComponent(promql)}`;
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
this.logger.error(`Prometheus query failed: ${response.status}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = (await response.json()) as PrometheusResponse;
|
||||
if (data.status !== 'success') {
|
||||
this.logger.error(`Prometheus query error: ${data.status}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
return data.data.result;
|
||||
} catch (error) {
|
||||
this.logger.error(`Prometheus query error: ${error}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getValue(promql: string): Promise<number | null> {
|
||||
const results = await this.query(promql);
|
||||
if (results.length === 0) return null;
|
||||
return parseFloat(results[0].value[1]);
|
||||
}
|
||||
|
||||
async getValues(promql: string): Promise<Map<string, number>> {
|
||||
const results = await this.query(promql);
|
||||
const values = new Map<string, number>();
|
||||
for (const result of results) {
|
||||
const label =
|
||||
result.metric.job || result.metric.datname || result.metric.instance || 'unknown';
|
||||
values.set(label, parseFloat(result.value[1]));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue