fix preprod
Some checks failed
CD Preprod / Backend — Lint (push) Successful in 10m22s
CD Preprod / Frontend — Lint & Type-check (push) Successful in 10m55s
CD Preprod / Backend — Unit Tests (push) Successful in 10m12s
CD Preprod / Frontend — Unit Tests (push) Successful in 10m33s
CD Preprod / Backend — Integration Tests (push) Successful in 9m57s
CD Preprod / Build Backend (push) Successful in 7m51s
CD Preprod / Build Log Exporter (push) Successful in 34s
CD Preprod / Build Frontend (push) Successful in 19m46s
CD Preprod / Deploy to Preprod (push) Failing after 1s
CD Preprod / Notify Failure (push) Has been skipped
CD Preprod / Smoke Tests (push) Has been skipped
CD Preprod / Notify Success (push) Has been skipped

This commit is contained in:
David 2026-04-06 14:21:32 +02:00
parent 850c23c164
commit bbf059cce9
5 changed files with 117 additions and 20 deletions

View File

@ -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: [

View File

@ -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<string>(
'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);
}
}
}

View File

@ -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 {}

View File

@ -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 : <strong>{error}</strong>
<br />
<span className="text-xs text-red-500">
Vérifiez que le container log-exporter est démarré sur{' '}
<code className="font-mono">{LOG_EXPORTER_URL}</code>
Vérifiez que le backend et le log-exporter sont démarrés.
</span>
</span>
</div>

View File

@ -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: >-