mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 07:39:39 +02:00
feat(todo): add Prometheus metrics and update docs
- Add MetricsModule with prom-client for todo backend - Add MetricsInterceptor for request tracking - Update COMMANDS.md with presi and storage commands - Update Grafana dashboards for backend monitoring Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
0a4e7e0f73
commit
4a236a7a1f
14 changed files with 1148 additions and 405 deletions
445
.claude/plans/apps/nutriphi/MVP-PLAN.md
Normal file
445
.claude/plans/apps/nutriphi/MVP-PLAN.md
Normal file
|
|
@ -0,0 +1,445 @@
|
|||
# NutriPhi MVP-Plan
|
||||
|
||||
> **Status:** Planung abgeschlossen
|
||||
> **Letzte Aktualisierung:** 2026-01-24
|
||||
|
||||
## Vision
|
||||
|
||||
NutriPhi ist eine datenschutzorientierte, KI-gestützte Ernährungs-Tracking-Web-App für gesundheitsbewusste Menschen. Sie ermöglicht das Erfassen von Mahlzeiten per Foto oder Text und liefert vollständige Nährwertanalysen mit personalisierten Empfehlungen.
|
||||
|
||||
---
|
||||
|
||||
## Entscheidungen (bestätigt)
|
||||
|
||||
| Bereich | Entscheidung |
|
||||
|---------|--------------|
|
||||
| **Zielgruppe** | Gesundheitsbewusste Menschen |
|
||||
| **Eingabemethoden** | Foto + Text (Sprache in Phase 2) |
|
||||
| **Analyse-Tiefe** | Vollständig (Kalorien, Makros, Vitamine, Mineralstoffe) |
|
||||
| **Tracking** | Tagesziele + Fortschrittsanzeige |
|
||||
| **KI-Modell** | Google Gemini (neuestes, günstiges Modell) |
|
||||
| **Portionsgröße** | KI-Schätzung + manuelle Korrektur |
|
||||
| **Plattform** | Web-only (SvelteKit, mobile-optimiert) |
|
||||
| **Monetarisierung** | ManaCore Credits |
|
||||
| **Favoriten** | Ja, häufige Mahlzeiten speichern |
|
||||
| **Statistiken** | Tagesübersicht, Wochen-Trend, Ziel-Fortschritt |
|
||||
| **Empfehlungen** | Einfache Hinweise + KI-Coaching |
|
||||
| **Datenschutz** | Maximum (lokale Verarbeitung, minimale Speicherung) |
|
||||
|
||||
---
|
||||
|
||||
## MVP Feature-Scope
|
||||
|
||||
### Muss (MVP v1.0)
|
||||
|
||||
#### Kern-Features
|
||||
- [ ] **Foto-Analyse**: Foto hochladen/aufnehmen, Gemini analysiert Mahlzeit
|
||||
- [ ] **Text-Eingabe**: Mahlzeit per Freitext beschreiben als Alternative
|
||||
- [ ] **Vollständige Nährwert-Anzeige**: Kalorien, Protein, Kohlenhydrate, Fett, Ballaststoffe, Vitamine (A, B, C, D, E, K), Mineralstoffe (Eisen, Calcium, Magnesium, etc.)
|
||||
- [ ] **Portionsschätzung**: KI schätzt Menge, Nutzer kann korrigieren (S/M/L oder Gramm)
|
||||
- [ ] **Mahlzeit speichern**: Analyse in Tagesverlauf speichern
|
||||
|
||||
#### Tracking & Ziele
|
||||
- [ ] **Tagesziele setzen**: Kalorienziel, optional Makro-Ziele
|
||||
- [ ] **Tagesübersicht**: Alle Mahlzeiten des Tages, Fortschrittsbalken
|
||||
- [ ] **Wochen-Trend**: Einfaches Diagramm der letzten 7 Tage
|
||||
|
||||
#### Favoriten & Historie
|
||||
- [ ] **Favoriten speichern**: Häufige Mahlzeiten mit einem Klick wiederverwenden
|
||||
- [ ] **Mahlzeiten-Historie**: Vergangene Einträge durchsuchen
|
||||
|
||||
#### Empfehlungen
|
||||
- [ ] **Einfache Hinweise**: "Du hast heute wenig Protein", "Vitamin C unter Tagesziel"
|
||||
- [ ] **KI-Coaching**: Personalisierte Tipps basierend auf Verlauf und Zielen
|
||||
|
||||
#### Datenschutz (Maximum)
|
||||
- [ ] **Foto-Löschung**: Fotos werden nach Analyse sofort gelöscht (nur Ergebnis gespeichert)
|
||||
- [ ] **Minimale Speicherung**: Nur notwendige Daten auf Server
|
||||
- [ ] **Lokale Verarbeitung**: Wo möglich clientseitig (z.B. Statistik-Berechnung)
|
||||
- [ ] **Export-Funktion**: Nutzer kann alle Daten exportieren (DSGVO)
|
||||
- [ ] **Lösch-Funktion**: Account und alle Daten vollständig löschen
|
||||
|
||||
#### Auth & Credits
|
||||
- [ ] **ManaCore Auth**: Login über zentralen Auth-Service
|
||||
- [ ] **Credit-System**: X Credits pro Analyse, Integration mit ManaCore Credits
|
||||
|
||||
### Sollte (v1.1)
|
||||
|
||||
- [ ] Spracheingabe (Speech-to-Text)
|
||||
- [ ] Barcode-Scanner für verpackte Lebensmittel
|
||||
- [ ] Wassertracking
|
||||
- [ ] PWA-Installation für Mobile-Erlebnis
|
||||
|
||||
### Könnte (v1.2+)
|
||||
|
||||
- [ ] Rezepterkennung (komplexe Gerichte)
|
||||
- [ ] Mahlzeiten-Planung
|
||||
- [ ] Integration mit Apple Health / Google Fit
|
||||
- [ ] Gemeinsames Tracking (Familie/Partner)
|
||||
- [ ] Allergen-Warnungen
|
||||
|
||||
---
|
||||
|
||||
## Technische Architektur
|
||||
|
||||
### Stack
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Frontend (SvelteKit) │
|
||||
│ - Mobile-optimiertes UI │
|
||||
│ - PWA-fähig │
|
||||
│ - Kamera-Zugriff via Web API │
|
||||
│ - Lokale Statistik-Berechnung │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Backend (NestJS) │
|
||||
│ - Gemini API Integration │
|
||||
│ - Nährwert-Datenbank │
|
||||
│ - Credit-Verwaltung │
|
||||
│ - Empfehlungs-Engine │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────┼─────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ PostgreSQL│ │ ManaCore │ │ Gemini │
|
||||
│ (Supabase)│ │ Auth │ │ API │
|
||||
└──────────┘ └──────────┘ └──────────┘
|
||||
```
|
||||
|
||||
### Projektstruktur
|
||||
|
||||
```
|
||||
apps/nutriphi/
|
||||
├── apps/
|
||||
│ ├── web/ # SvelteKit (mobile-optimiert)
|
||||
│ └── backend/ # NestJS API
|
||||
├── packages/
|
||||
│ └── shared/ # Gemeinsame Types, Utils
|
||||
└── CLAUDE.md
|
||||
```
|
||||
|
||||
### Datenmodell (Entwurf)
|
||||
|
||||
```typescript
|
||||
// User Ziele
|
||||
interface UserGoals {
|
||||
id: string;
|
||||
userId: string;
|
||||
dailyCalories: number;
|
||||
dailyProtein?: number; // in Gramm
|
||||
dailyCarbs?: number;
|
||||
dailyFat?: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
// Mahlzeit
|
||||
interface Meal {
|
||||
id: string;
|
||||
userId: string;
|
||||
date: Date;
|
||||
mealType: 'breakfast' | 'lunch' | 'dinner' | 'snack';
|
||||
inputType: 'photo' | 'text';
|
||||
description: string; // KI-generierte Beschreibung
|
||||
// Foto wird NICHT gespeichert (Datenschutz)
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
// Nährwerte pro Mahlzeit
|
||||
interface MealNutrition {
|
||||
mealId: string;
|
||||
calories: number;
|
||||
protein: number;
|
||||
carbohydrates: number;
|
||||
fat: number;
|
||||
fiber: number;
|
||||
sugar: number;
|
||||
// Vitamine (in mg oder µg)
|
||||
vitaminA?: number;
|
||||
vitaminB1?: number;
|
||||
vitaminB2?: number;
|
||||
vitaminB6?: number;
|
||||
vitaminB12?: number;
|
||||
vitaminC?: number;
|
||||
vitaminD?: number;
|
||||
vitaminE?: number;
|
||||
vitaminK?: number;
|
||||
// Mineralstoffe (in mg)
|
||||
calcium?: number;
|
||||
iron?: number;
|
||||
magnesium?: number;
|
||||
potassium?: number;
|
||||
sodium?: number;
|
||||
zinc?: number;
|
||||
}
|
||||
|
||||
// Favoriten
|
||||
interface FavoriteMeal {
|
||||
id: string;
|
||||
userId: string;
|
||||
name: string;
|
||||
description: string;
|
||||
nutrition: MealNutrition;
|
||||
usageCount: number;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
// Empfehlungen
|
||||
interface DailyRecommendation {
|
||||
id: string;
|
||||
userId: string;
|
||||
date: Date;
|
||||
type: 'hint' | 'coaching';
|
||||
message: string;
|
||||
nutrient?: string; // z.B. 'protein', 'vitaminC'
|
||||
createdAt: Date;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## KI-Integration (Gemini)
|
||||
|
||||
### Prompt-Strategie
|
||||
|
||||
```
|
||||
System: Du bist ein Ernährungsexperte. Analysiere das Bild/die Beschreibung
|
||||
einer Mahlzeit und liefere eine detaillierte Nährwertanalyse.
|
||||
|
||||
Aufgaben:
|
||||
1. Identifiziere alle Lebensmittel im Bild/Text
|
||||
2. Schätze die Portionsgröße (in Gramm)
|
||||
3. Berechne Nährwerte basierend auf Standard-Datenbanken
|
||||
4. Gib Konfidenz-Score für die Schätzung an
|
||||
|
||||
Output-Format: JSON mit strukturierten Nährwertdaten
|
||||
```
|
||||
|
||||
### Kosten-Schätzung (Gemini)
|
||||
|
||||
| Modell | Kosten | Use Case |
|
||||
|--------|--------|----------|
|
||||
| Gemini 1.5 Flash | ~$0.001/Analyse | Standard-Analysen |
|
||||
| Gemini 1.5 Pro | ~$0.01/Analyse | Komplexe Gerichte, Coaching |
|
||||
|
||||
### Credit-Mapping
|
||||
|
||||
| Aktion | Credits |
|
||||
|--------|---------|
|
||||
| Foto-Analyse | 5 Credits |
|
||||
| Text-Analyse | 2 Credits |
|
||||
| KI-Coaching Anfrage | 10 Credits |
|
||||
|
||||
---
|
||||
|
||||
## Datenschutz-Implementierung
|
||||
|
||||
### Foto-Handling
|
||||
|
||||
```
|
||||
1. Nutzer macht Foto
|
||||
2. Foto wird direkt an Gemini gesendet (Base64)
|
||||
3. Analyse-Ergebnis wird gespeichert
|
||||
4. Foto wird NICHT gespeichert
|
||||
5. Kein Foto-Caching auf Server
|
||||
```
|
||||
|
||||
### Daten-Export (DSGVO Art. 20)
|
||||
|
||||
- JSON-Export aller Nutzerdaten
|
||||
- PDF-Report mit Statistiken
|
||||
- Ein-Klick Download
|
||||
|
||||
### Account-Löschung (DSGVO Art. 17)
|
||||
|
||||
- Sofortige Löschung aller Daten
|
||||
- Keine Backups nach 30 Tagen
|
||||
- Bestätigungs-E-Mail
|
||||
|
||||
---
|
||||
|
||||
## UI/UX Konzept
|
||||
|
||||
### Mobile-First Design
|
||||
|
||||
```
|
||||
┌─────────────────────────────┐
|
||||
│ NutriPhi [+] [Profile] │
|
||||
├─────────────────────────────┤
|
||||
│ │
|
||||
│ Heute: 1.450 / 2.000 kcal │
|
||||
│ ████████████░░░░░ 72% │
|
||||
│ │
|
||||
│ Protein Carbs Fett │
|
||||
│ 85g/120 140g/200 45g/70 │
|
||||
│ │
|
||||
├─────────────────────────────┤
|
||||
│ Frühstück 420 kcal │
|
||||
│ Mittagessen 680 kcal │
|
||||
│ Snack 350 kcal │
|
||||
│ │
|
||||
├─────────────────────────────┤
|
||||
│ │
|
||||
│ [📷 Foto] [✏️ Text] │
|
||||
│ │
|
||||
└─────────────────────────────┘
|
||||
```
|
||||
|
||||
### Farbschema
|
||||
|
||||
- Primary: Grün (#22C55E) - Gesundheit, Natur
|
||||
- Secondary: Orange (#F97316) - Energie, Wärme
|
||||
- Background: Warm White (#FAFAF9)
|
||||
- Accent: Teal (#14B8A6)
|
||||
|
||||
---
|
||||
|
||||
## Entwicklungs-Roadmap
|
||||
|
||||
### Phase 1: Foundation (Woche 1-2)
|
||||
- [ ] Projekt-Setup im Monorepo
|
||||
- [ ] Datenbank-Schema erstellen
|
||||
- [ ] Auth-Integration
|
||||
- [ ] Basis-UI Komponenten
|
||||
|
||||
### Phase 2: Kern-Features (Woche 3-4)
|
||||
- [ ] Gemini API Integration
|
||||
- [ ] Foto-Analyse Flow
|
||||
- [ ] Text-Eingabe Flow
|
||||
- [ ] Nährwert-Anzeige
|
||||
|
||||
### Phase 3: Tracking (Woche 5-6)
|
||||
- [ ] Tagesübersicht
|
||||
- [ ] Ziele setzen
|
||||
- [ ] Statistiken
|
||||
- [ ] Favoriten
|
||||
|
||||
### Phase 4: Intelligence (Woche 7-8)
|
||||
- [ ] Empfehlungs-Engine
|
||||
- [ ] KI-Coaching
|
||||
- [ ] Wochen-Trends
|
||||
|
||||
### Phase 5: Polish (Woche 9-10)
|
||||
- [ ] Datenschutz-Features (Export, Löschung)
|
||||
- [ ] PWA-Optimierung
|
||||
- [ ] Performance
|
||||
- [ ] Testing
|
||||
|
||||
---
|
||||
|
||||
## Offene Punkte
|
||||
|
||||
- [ ] Gemini API Key beschaffen und konfigurieren (User richtet ein)
|
||||
- [x] Nährwert-Referenzdatenbank: **Hybrid-Ansatz** (siehe unten)
|
||||
- [x] Design-System: Shared Components aus `@manacore/shared-landing-ui`
|
||||
- [x] Landing Page: Ja, mit Astro (wie andere Apps)
|
||||
|
||||
---
|
||||
|
||||
## Nährwert-Datenbank: Hybrid-Ansatz
|
||||
|
||||
### Entscheidung
|
||||
|
||||
| Datenbank | Verwendung |
|
||||
|-----------|------------|
|
||||
| **USDA FoodData Central** | Grundnahrungsmittel, präzise Mikronährstoffe |
|
||||
| **Open Food Facts** | Verpackte Produkte, deutsche Marken (REWE, Lidl, Aldi) |
|
||||
| **Gemini Fallback** | Wenn keine DB-Match, KI schätzt selbst |
|
||||
|
||||
### Ablauf
|
||||
|
||||
```
|
||||
1. Gemini analysiert Foto → identifiziert Lebensmittel
|
||||
2. Backend sucht in USDA (Grundnahrungsmittel) oder Open Food Facts (Markenprodukte)
|
||||
3. Nährwerte werden aus DB geholt oder von Gemini geschätzt
|
||||
4. Konfidenz-Score zeigt Datenqualität an
|
||||
```
|
||||
|
||||
### Vorteile
|
||||
|
||||
- USDA: 150+ Nährstoffe, laborgeprüft
|
||||
- Open Food Facts: 3 Mio. Produkte, viele deutsche
|
||||
- Gemini: Intelligenter Fallback für unbekannte Gerichte
|
||||
|
||||
---
|
||||
|
||||
## Landing Page (Astro)
|
||||
|
||||
### Tech Stack
|
||||
|
||||
- **Framework:** Astro 5.x
|
||||
- **Styling:** Tailwind CSS
|
||||
- **Shared Components:** `@manacore/shared-landing-ui`
|
||||
- **Deployment:** Cloudflare Pages
|
||||
|
||||
### Wiederverwendbare Shared Components
|
||||
|
||||
| Komponente | Verwendung in NutriPhi |
|
||||
|------------|------------------------|
|
||||
| `HeroSection` | "Fotografiere dein Essen, verstehe was du isst" |
|
||||
| `FeatureSection` | KI-Analyse, Nährwerte, Tracking, Empfehlungen |
|
||||
| `StepsSection` | Foto → Analyse → Tracking |
|
||||
| `PricingSection` | Free/Pro mit Credit-System |
|
||||
| `FAQSection` | Datenschutz, Genauigkeit, Diäten |
|
||||
| `CTASection` | "Jetzt kostenlos starten" |
|
||||
| `Navigation` | Shared Header |
|
||||
| `Footer` | Shared Footer |
|
||||
|
||||
### Projektstruktur
|
||||
|
||||
```
|
||||
apps/nutriphi/apps/landing/
|
||||
├── src/
|
||||
│ ├── pages/
|
||||
│ │ └── index.astro
|
||||
│ ├── layouts/
|
||||
│ │ └── Layout.astro
|
||||
│ ├── styles/
|
||||
│ │ └── global.css # NutriPhi Theme (Grün)
|
||||
│ └── components/ # Custom falls nötig
|
||||
├── astro.config.mjs
|
||||
├── package.json
|
||||
└── wrangler.toml # Cloudflare Deploy
|
||||
```
|
||||
|
||||
### Farbschema (CSS Custom Properties)
|
||||
|
||||
```css
|
||||
/* NutriPhi Theme - Gesundheit/Natur */
|
||||
--color-primary: #22C55E; /* Green 500 */
|
||||
--color-primary-hover: #16A34A; /* Green 600 */
|
||||
--color-secondary: #F97316; /* Orange 500 */
|
||||
--color-accent: #14B8A6; /* Teal 500 */
|
||||
--color-background-page: #0F1F0F; /* Dark Green tinted */
|
||||
--color-background-card: #1A2F1A;
|
||||
--color-text-primary: #F0FDF4; /* Green 50 */
|
||||
--color-text-secondary: #BBF7D0; /* Green 200 */
|
||||
```
|
||||
|
||||
### Seitenstruktur
|
||||
|
||||
```
|
||||
Navigation
|
||||
├── Hero: "Fotografiere dein Essen. Verstehe deinen Körper."
|
||||
│ ├── Trust Badges: Datenschutz-First, KI-Powered, Kostenlos starten
|
||||
│ └── CTA: Jetzt starten / Mehr erfahren
|
||||
├── Features (6 Cards, 3 Spalten)
|
||||
│ ├── Foto-Analyse
|
||||
│ ├── Vollständige Nährwerte
|
||||
│ ├── Tagesziele
|
||||
│ ├── KI-Coaching
|
||||
│ ├── Favoriten
|
||||
│ └── Datenschutz
|
||||
├── Steps: Wie es funktioniert (3 Schritte)
|
||||
│ ├── 1. Foto machen
|
||||
│ ├── 2. KI analysiert
|
||||
│ └── 3. Insights erhalten
|
||||
├── Pricing (Free / Pro)
|
||||
├── FAQ (6 Fragen)
|
||||
└── CTA: Starte jetzt kostenlos
|
||||
Footer
|
||||
```
|
||||
22
.claude/plans/apps/nutriphi/README.md
Normal file
22
.claude/plans/apps/nutriphi/README.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# NutriPhi - App-Planung
|
||||
|
||||
## Vision
|
||||
|
||||
NutriPhi ist eine KI-gestützte Ernährungs-Tracking-App, die es Nutzern ermöglicht, ihre Mahlzeiten per Foto, Text oder Sprache zu erfassen und automatisch Nährwertinformationen zu erhalten.
|
||||
|
||||
## Planungsdokumente
|
||||
|
||||
| Dokument | Status | Beschreibung |
|
||||
|----------|--------|--------------|
|
||||
| [MVP-PLAN.md](./MVP-PLAN.md) | In Arbeit | Kernfunktionen für den MVP |
|
||||
| [TECHNICAL-ARCHITECTURE.md](./TECHNICAL-ARCHITECTURE.md) | Ausstehend | Technische Architektur |
|
||||
| [AI-INTEGRATION.md](./AI-INTEGRATION.md) | Ausstehend | KI-Modelle und Bildanalyse |
|
||||
| [DATA-MODEL.md](./DATA-MODEL.md) | Ausstehend | Datenbankschema |
|
||||
|
||||
## Projektstatus
|
||||
|
||||
- [x] MVP-Planung abgeschlossen
|
||||
- [x] Technische Architektur definiert
|
||||
- [x] KI-Integration geplant
|
||||
- [x] Datenmodell erstellt
|
||||
- [ ] Entwicklung gestartet
|
||||
|
|
@ -11,6 +11,8 @@ pnpm dev:picture:full
|
|||
pnpm dev:calendar:full
|
||||
pnpm dev:contacts:full
|
||||
pnpm dev:todo:full
|
||||
pnpm dev:presi:full
|
||||
pnpm dev:storage:full
|
||||
|
||||
pnpm dev:manacore:app # Nur ManaCore Web
|
||||
pnpm dev:manacore:backends # Alle 7 Backends für Dashboard-Widgets
|
||||
|
|
|
|||
|
|
@ -32,7 +32,8 @@
|
|||
"postgres": "^3.4.5",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rrule": "^2.8.1",
|
||||
"rxjs": "^7.8.1"
|
||||
"rxjs": "^7.8.1",
|
||||
"prom-client": "^15.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.4.9",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { DatabaseModule } from './db/database.module';
|
||||
import { HealthModule } from './health/health.module';
|
||||
import { ProjectModule } from './project/project.module';
|
||||
|
|
@ -9,6 +10,7 @@ import { LabelModule } from './label/label.module';
|
|||
import { ReminderModule } from './reminder/reminder.module';
|
||||
import { KanbanModule } from './kanban/kanban.module';
|
||||
import { NetworkModule } from './network/network.module';
|
||||
import { MetricsModule, MetricsInterceptor } from './metrics';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -17,6 +19,7 @@ import { NetworkModule } from './network/network.module';
|
|||
envFilePath: '.env',
|
||||
}),
|
||||
ScheduleModule.forRoot(),
|
||||
MetricsModule,
|
||||
DatabaseModule,
|
||||
HealthModule,
|
||||
ProjectModule,
|
||||
|
|
@ -26,5 +29,11 @@ import { NetworkModule } from './network/network.module';
|
|||
KanbanModule,
|
||||
NetworkModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: MetricsInterceptor,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
|
|
|||
|
|
@ -52,8 +52,10 @@ async function bootstrap() {
|
|||
})
|
||||
);
|
||||
|
||||
// API prefix
|
||||
app.setGlobalPrefix('api/v1');
|
||||
// API prefix (exclude metrics endpoint for Prometheus scraping)
|
||||
app.setGlobalPrefix('api/v1', {
|
||||
exclude: ['metrics', 'health'],
|
||||
});
|
||||
|
||||
const port = process.env.PORT || 3017;
|
||||
await app.listen(port);
|
||||
|
|
|
|||
4
apps/todo/apps/backend/src/metrics/index.ts
Normal file
4
apps/todo/apps/backend/src/metrics/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export * from './metrics.module';
|
||||
export * from './metrics.service';
|
||||
export * from './metrics.interceptor';
|
||||
export * from './metrics.controller';
|
||||
14
apps/todo/apps/backend/src/metrics/metrics.controller.ts
Normal file
14
apps/todo/apps/backend/src/metrics/metrics.controller.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { Controller, Get, Header, Res } from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
import { MetricsService } from './metrics.service';
|
||||
|
||||
@Controller()
|
||||
export class MetricsController {
|
||||
constructor(private readonly metricsService: MetricsService) {}
|
||||
|
||||
@Get('metrics')
|
||||
async getMetrics(@Res() res: Response): Promise<void> {
|
||||
res.set('Content-Type', this.metricsService.getContentType());
|
||||
res.send(await this.metricsService.getMetrics());
|
||||
}
|
||||
}
|
||||
73
apps/todo/apps/backend/src/metrics/metrics.interceptor.ts
Normal file
73
apps/todo/apps/backend/src/metrics/metrics.interceptor.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import {
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
ExecutionContext,
|
||||
CallHandler,
|
||||
} from '@nestjs/common';
|
||||
import { Observable, tap } from 'rxjs';
|
||||
import { Request, Response } from 'express';
|
||||
import { MetricsService } from './metrics.service';
|
||||
|
||||
@Injectable()
|
||||
export class MetricsInterceptor implements NestInterceptor {
|
||||
constructor(private readonly metricsService: MetricsService) {}
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
|
||||
const httpContext = context.switchToHttp();
|
||||
const request = httpContext.getRequest<Request>();
|
||||
const response = httpContext.getResponse<Response>();
|
||||
|
||||
// Skip metrics endpoint itself
|
||||
if (request.path === '/metrics') {
|
||||
return next.handle();
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const method = request.method;
|
||||
// Normalize route (remove IDs to prevent high cardinality)
|
||||
const route = this.normalizeRoute(request.path);
|
||||
|
||||
return next.handle().pipe(
|
||||
tap({
|
||||
next: () => {
|
||||
this.recordMetrics(method, route, response.statusCode, startTime);
|
||||
},
|
||||
error: () => {
|
||||
const status = response.statusCode >= 400 ? response.statusCode : 500;
|
||||
this.recordMetrics(method, route, status, startTime);
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private recordMetrics(
|
||||
method: string,
|
||||
route: string,
|
||||
status: number,
|
||||
startTime: number
|
||||
): void {
|
||||
const duration = (Date.now() - startTime) / 1000;
|
||||
const statusStr = status.toString();
|
||||
|
||||
this.metricsService.httpRequestsTotal.inc({
|
||||
method,
|
||||
route,
|
||||
status: statusStr,
|
||||
});
|
||||
|
||||
this.metricsService.httpRequestDuration.observe(
|
||||
{ method, route, status: statusStr },
|
||||
duration
|
||||
);
|
||||
}
|
||||
|
||||
private normalizeRoute(path: string): string {
|
||||
// Replace UUIDs and numeric IDs with placeholders
|
||||
return path
|
||||
.replace(
|
||||
/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi,
|
||||
':id'
|
||||
)
|
||||
.replace(/\/\d+/g, '/:id');
|
||||
}
|
||||
}
|
||||
11
apps/todo/apps/backend/src/metrics/metrics.module.ts
Normal file
11
apps/todo/apps/backend/src/metrics/metrics.module.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { Module, Global } from '@nestjs/common';
|
||||
import { MetricsService } from './metrics.service';
|
||||
import { MetricsController } from './metrics.controller';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
controllers: [MetricsController],
|
||||
providers: [MetricsService],
|
||||
exports: [MetricsService],
|
||||
})
|
||||
export class MetricsModule {}
|
||||
67
apps/todo/apps/backend/src/metrics/metrics.service.ts
Normal file
67
apps/todo/apps/backend/src/metrics/metrics.service.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import * as client from 'prom-client';
|
||||
|
||||
@Injectable()
|
||||
export class MetricsService implements OnModuleInit {
|
||||
private readonly register: client.Registry;
|
||||
|
||||
// HTTP metrics
|
||||
readonly httpRequestsTotal: client.Counter<string>;
|
||||
readonly httpRequestDuration: client.Histogram<string>;
|
||||
|
||||
// Business metrics
|
||||
readonly tasksCreated: client.Counter<string>;
|
||||
readonly tasksCompleted: client.Counter<string>;
|
||||
|
||||
constructor() {
|
||||
this.register = new client.Registry();
|
||||
|
||||
// Add default metrics (CPU, memory, event loop, etc.)
|
||||
client.collectDefaultMetrics({
|
||||
register: this.register,
|
||||
prefix: 'todo_',
|
||||
});
|
||||
|
||||
// HTTP request counter
|
||||
this.httpRequestsTotal = new client.Counter({
|
||||
name: 'http_requests_total',
|
||||
help: 'Total number of HTTP requests',
|
||||
labelNames: ['method', 'route', 'status'],
|
||||
registers: [this.register],
|
||||
});
|
||||
|
||||
// HTTP request duration histogram
|
||||
this.httpRequestDuration = new client.Histogram({
|
||||
name: 'http_request_duration_seconds',
|
||||
help: 'Duration of HTTP requests in seconds',
|
||||
labelNames: ['method', 'route', 'status'],
|
||||
buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 2, 5],
|
||||
registers: [this.register],
|
||||
});
|
||||
|
||||
// Business metrics
|
||||
this.tasksCreated = new client.Counter({
|
||||
name: 'todo_tasks_created_total',
|
||||
help: 'Total number of tasks created',
|
||||
registers: [this.register],
|
||||
});
|
||||
|
||||
this.tasksCompleted = new client.Counter({
|
||||
name: 'todo_tasks_completed_total',
|
||||
help: 'Total number of tasks completed',
|
||||
registers: [this.register],
|
||||
});
|
||||
}
|
||||
|
||||
onModuleInit() {
|
||||
// Metrics are ready
|
||||
}
|
||||
|
||||
async getMetrics(): Promise<string> {
|
||||
return this.register.metrics();
|
||||
}
|
||||
|
||||
getContentType(): string {
|
||||
return this.register.contentType;
|
||||
}
|
||||
}
|
||||
|
|
@ -61,8 +61,8 @@
|
|||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||
"expr": "rate(container_cpu_usage_seconds_total{name=~\".+\"}[5m]) * 100",
|
||||
"legendFormat": "{{name}}",
|
||||
"expr": "sum(rate(container_cpu_usage_seconds_total{id=~\"/docker/.+\"}[5m])) by (id) * 100",
|
||||
"legendFormat": "{{id}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
|
|
@ -114,8 +114,8 @@
|
|||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||
"expr": "container_memory_usage_bytes{name=~\".+\"}",
|
||||
"legendFormat": "{{name}}",
|
||||
"expr": "container_memory_usage_bytes{id=~\"/docker/.+\"}",
|
||||
"legendFormat": "{{id}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@
|
|||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||
"expr": "(1 - (node_filesystem_avail_bytes{mountpoint=\"/\"} / node_filesystem_size_bytes{mountpoint=\"/\"})) * 100",
|
||||
"expr": "(1 - (node_filesystem_avail_bytes{mountpoint=~\"/host_mnt/Users|/\"} / node_filesystem_size_bytes{mountpoint=~\"/host_mnt/Users|/\"})) * 100",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
|
|
@ -177,7 +177,7 @@
|
|||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||
"expr": "count(container_last_seen{name=~\".+\"})",
|
||||
"expr": "count(container_last_seen{id=~\"/docker/.+\"})",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
|
|
|
|||
885
pnpm-lock.yaml
generated
885
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue