From bbf059cce99cfffdbab0857424bd53ab3234e916 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 6 Apr 2026 14:21:32 +0200 Subject: [PATCH] fix preprod --- apps/backend/src/app.module.ts | 3 + .../src/application/logs/logs.controller.ts | 98 +++++++++++++++++++ .../src/application/logs/logs.module.ts | 9 ++ .../app/dashboard/admin/logs/page.tsx | 13 ++- infra/logging/promtail/promtail-config.yml | 14 +-- 5 files changed, 117 insertions(+), 20 deletions(-) create mode 100644 apps/backend/src/application/logs/logs.controller.ts create mode 100644 apps/backend/src/application/logs/logs.module.ts diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index 7e7ada3..0ac03c6 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -19,6 +19,7 @@ import { WebhooksModule } from './application/webhooks/webhooks.module'; import { GDPRModule } from './application/gdpr/gdpr.module'; import { CsvBookingsModule } from './application/csv-bookings.module'; import { AdminModule } from './application/admin/admin.module'; +import { LogsModule } from './application/logs/logs.module'; import { SubscriptionsModule } from './application/subscriptions/subscriptions.module'; import { ApiKeysModule } from './application/api-keys/api-keys.module'; import { CacheModule } from './infrastructure/cache/cache.module'; @@ -67,6 +68,7 @@ import { CustomThrottlerGuard } from './application/guards/throttle.guard'; STRIPE_GOLD_YEARLY_PRICE_ID: Joi.string().optional(), STRIPE_PLATINIUM_MONTHLY_PRICE_ID: Joi.string().optional(), STRIPE_PLATINIUM_YEARLY_PRICE_ID: Joi.string().optional(), + LOG_EXPORTER_URL: Joi.string().uri().default('http://xpeditis-log-exporter:3200'), }), }), @@ -147,6 +149,7 @@ import { CustomThrottlerGuard } from './application/guards/throttle.guard'; AdminModule, SubscriptionsModule, ApiKeysModule, + LogsModule, ], controllers: [], providers: [ diff --git a/apps/backend/src/application/logs/logs.controller.ts b/apps/backend/src/application/logs/logs.controller.ts new file mode 100644 index 0000000..1926c1e --- /dev/null +++ b/apps/backend/src/application/logs/logs.controller.ts @@ -0,0 +1,98 @@ +import { + Controller, + Get, + Query, + Res, + UseGuards, + HttpException, + HttpStatus, +} from '@nestjs/common'; +import { Response } from 'express'; +import { ConfigService } from '@nestjs/config'; +import { JwtAuthGuard } from '../guards/jwt-auth.guard'; +import { RolesGuard } from '../guards/roles.guard'; +import { Roles } from '../decorators/roles.decorator'; + +@Controller('logs') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('admin') +export class LogsController { + private readonly logExporterUrl: string; + + constructor(private readonly configService: ConfigService) { + this.logExporterUrl = this.configService.get( + 'LOG_EXPORTER_URL', + 'http://xpeditis-log-exporter:3200', + ); + } + + /** + * GET /api/v1/logs/services + * Proxy → log-exporter /api/logs/services + */ + @Get('services') + async getServices() { + try { + const res = await fetch(`${this.logExporterUrl}/api/logs/services`, { + signal: AbortSignal.timeout(5000), + }); + if (!res.ok) throw new Error(`log-exporter error: ${res.status}`); + return res.json(); + } catch (err: any) { + throw new HttpException( + { error: err.message }, + HttpStatus.BAD_GATEWAY, + ); + } + } + + /** + * GET /api/v1/logs/export + * Proxy → log-exporter /api/logs/export (JSON or CSV) + */ + @Get('export') + async exportLogs( + @Query('service') service: string, + @Query('level') level: string, + @Query('search') search: string, + @Query('start') start: string, + @Query('end') end: string, + @Query('limit') limit: string, + @Query('format') format: string = 'json', + @Res() res: Response, + ) { + try { + const params = new URLSearchParams(); + if (service) params.set('service', service); + if (level) params.set('level', level); + if (search) params.set('search', search); + if (start) params.set('start', start); + if (end) params.set('end', end); + if (limit) params.set('limit', limit); + params.set('format', format); + + const upstream = await fetch( + `${this.logExporterUrl}/api/logs/export?${params}`, + { signal: AbortSignal.timeout(30000) }, + ); + + if (!upstream.ok) { + const body = await upstream.json().catch(() => ({})); + throw new HttpException(body, upstream.status); + } + + res.status(upstream.status); + upstream.headers.forEach((value, key) => { + if (['content-type', 'content-disposition'].includes(key.toLowerCase())) { + res.setHeader(key, value); + } + }); + + const buffer = await upstream.arrayBuffer(); + res.send(Buffer.from(buffer)); + } catch (err: any) { + if (err instanceof HttpException) throw err; + throw new HttpException({ error: err.message }, HttpStatus.BAD_GATEWAY); + } + } +} diff --git a/apps/backend/src/application/logs/logs.module.ts b/apps/backend/src/application/logs/logs.module.ts new file mode 100644 index 0000000..ca12157 --- /dev/null +++ b/apps/backend/src/application/logs/logs.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { LogsController } from './logs.controller'; + +@Module({ + imports: [ConfigModule], + controllers: [LogsController], +}) +export class LogsModule {} diff --git a/apps/frontend/app/dashboard/admin/logs/page.tsx b/apps/frontend/app/dashboard/admin/logs/page.tsx index edf04ed..9d5c6f2 100644 --- a/apps/frontend/app/dashboard/admin/logs/page.tsx +++ b/apps/frontend/app/dashboard/admin/logs/page.tsx @@ -12,8 +12,8 @@ import { Server, } from 'lucide-react'; -const LOG_EXPORTER_URL = - process.env.NEXT_PUBLIC_LOG_EXPORTER_URL || 'http://localhost:3200'; +const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'; +const LOGS_API_URL = `${API_URL}/api/v1/logs`; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -125,7 +125,7 @@ export default function AdminLogsPage() { // Load available services useEffect(() => { - fetch(`${LOG_EXPORTER_URL}/api/logs/services`) + fetch(`${LOGS_API_URL}/services`) .then(r => r.json()) .then(d => setServices(d.services || [])) .catch(() => {}); @@ -151,7 +151,7 @@ export default function AdminLogsPage() { setError(null); try { const res = await fetch( - `${LOG_EXPORTER_URL}/api/logs/export?${buildQueryString('json')}`, + `${LOGS_API_URL}/export?${buildQueryString('json')}`, ); if (!res.ok) { const body = await res.json().catch(() => ({})); @@ -175,7 +175,7 @@ export default function AdminLogsPage() { setExportLoading(true); try { const res = await fetch( - `${LOG_EXPORTER_URL}/api/logs/export?${buildQueryString(format)}`, + `${LOGS_API_URL}/export?${buildQueryString(format)}`, ); if (!res.ok) throw new Error(`HTTP ${res.status}`); const blob = await res.blob(); @@ -384,8 +384,7 @@ export default function AdminLogsPage() { Impossible de contacter le log-exporter : {error}
- Vérifiez que le container log-exporter est démarré sur{' '} - {LOG_EXPORTER_URL} + Vérifiez que le backend et le log-exporter sont démarrés. diff --git a/infra/logging/promtail/promtail-config.yml b/infra/logging/promtail/promtail-config.yml index 453e222..67c5b6b 100644 --- a/infra/logging/promtail/promtail-config.yml +++ b/infra/logging/promtail/promtail-config.yml @@ -1,53 +1,43 @@ server: http_listen_port: 9080 - grpc_listen_port: 0 log_level: warn positions: filename: /tmp/positions.yaml clients: - - url: http://loki:3100/loki/api/v1/push + - url: http://xpeditis-loki:3100/loki/api/v1/push batchwait: 1s batchsize: 1048576 timeout: 10s scrape_configs: - # ─── Docker container log collection (Mac-compatible via Docker socket API) ─ - job_name: docker docker_sd_configs: - host: unix:///var/run/docker.sock refresh_interval: 5s filters: - # Only collect containers with label: logging=promtail - # Add this label to backend and frontend in docker-compose.dev.yml - name: label values: ['logging=promtail'] relabel_configs: - # Use docker-compose service name as the "service" label - source_labels: ['__meta_docker_container_label_com_docker_compose_service'] target_label: service - # Keep container name for context - source_labels: ['__meta_docker_container_name'] regex: '/?(.*)' replacement: '${1}' target_label: container - # Log stream (stdout / stderr) - source_labels: ['__meta_docker_container_log_stream'] target_label: stream pipeline_stages: - # Drop entries older than 15 min to avoid replaying full container log history - drop: older_than: 15m drop_counter_reason: entry_too_old - # Drop noisy health-check / ping lines - drop: expression: 'GET /(health|metrics|minio/health)' - # Try to parse JSON (NestJS/pino output) - json: expressions: level: level @@ -55,12 +45,10 @@ scrape_configs: context: context reqId: reqId - # Promote parsed fields as Loki labels - labels: level: context: - # Map pino numeric levels to strings - template: source: level template: >-