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 { GDPRModule } from './application/gdpr/gdpr.module';
import { CsvBookingsModule } from './application/csv-bookings.module'; import { CsvBookingsModule } from './application/csv-bookings.module';
import { AdminModule } from './application/admin/admin.module'; import { AdminModule } from './application/admin/admin.module';
import { LogsModule } from './application/logs/logs.module';
import { SubscriptionsModule } from './application/subscriptions/subscriptions.module'; import { SubscriptionsModule } from './application/subscriptions/subscriptions.module';
import { ApiKeysModule } from './application/api-keys/api-keys.module'; import { ApiKeysModule } from './application/api-keys/api-keys.module';
import { CacheModule } from './infrastructure/cache/cache.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_GOLD_YEARLY_PRICE_ID: Joi.string().optional(),
STRIPE_PLATINIUM_MONTHLY_PRICE_ID: Joi.string().optional(), STRIPE_PLATINIUM_MONTHLY_PRICE_ID: Joi.string().optional(),
STRIPE_PLATINIUM_YEARLY_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, AdminModule,
SubscriptionsModule, SubscriptionsModule,
ApiKeysModule, ApiKeysModule,
LogsModule,
], ],
controllers: [], controllers: [],
providers: [ 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, Server,
} from 'lucide-react'; } from 'lucide-react';
const LOG_EXPORTER_URL = const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000';
process.env.NEXT_PUBLIC_LOG_EXPORTER_URL || 'http://localhost:3200'; const LOGS_API_URL = `${API_URL}/api/v1/logs`;
// ─── Types ──────────────────────────────────────────────────────────────────── // ─── Types ────────────────────────────────────────────────────────────────────
@ -125,7 +125,7 @@ export default function AdminLogsPage() {
// Load available services // Load available services
useEffect(() => { useEffect(() => {
fetch(`${LOG_EXPORTER_URL}/api/logs/services`) fetch(`${LOGS_API_URL}/services`)
.then(r => r.json()) .then(r => r.json())
.then(d => setServices(d.services || [])) .then(d => setServices(d.services || []))
.catch(() => {}); .catch(() => {});
@ -151,7 +151,7 @@ export default function AdminLogsPage() {
setError(null); setError(null);
try { try {
const res = await fetch( const res = await fetch(
`${LOG_EXPORTER_URL}/api/logs/export?${buildQueryString('json')}`, `${LOGS_API_URL}/export?${buildQueryString('json')}`,
); );
if (!res.ok) { if (!res.ok) {
const body = await res.json().catch(() => ({})); const body = await res.json().catch(() => ({}));
@ -175,7 +175,7 @@ export default function AdminLogsPage() {
setExportLoading(true); setExportLoading(true);
try { try {
const res = await fetch( 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}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
const blob = await res.blob(); const blob = await res.blob();
@ -384,8 +384,7 @@ export default function AdminLogsPage() {
Impossible de contacter le log-exporter : <strong>{error}</strong> Impossible de contacter le log-exporter : <strong>{error}</strong>
<br /> <br />
<span className="text-xs text-red-500"> <span className="text-xs text-red-500">
Vérifiez que le container log-exporter est démarré sur{' '} Vérifiez que le backend et le log-exporter sont démarrés.
<code className="font-mono">{LOG_EXPORTER_URL}</code>
</span> </span>
</span> </span>
</div> </div>

View File

@ -1,53 +1,43 @@
server: server:
http_listen_port: 9080 http_listen_port: 9080
grpc_listen_port: 0
log_level: warn log_level: warn
positions: positions:
filename: /tmp/positions.yaml filename: /tmp/positions.yaml
clients: clients:
- url: http://loki:3100/loki/api/v1/push - url: http://xpeditis-loki:3100/loki/api/v1/push
batchwait: 1s batchwait: 1s
batchsize: 1048576 batchsize: 1048576
timeout: 10s timeout: 10s
scrape_configs: scrape_configs:
# ─── Docker container log collection (Mac-compatible via Docker socket API) ─
- job_name: docker - job_name: docker
docker_sd_configs: docker_sd_configs:
- host: unix:///var/run/docker.sock - host: unix:///var/run/docker.sock
refresh_interval: 5s refresh_interval: 5s
filters: filters:
# Only collect containers with label: logging=promtail
# Add this label to backend and frontend in docker-compose.dev.yml
- name: label - name: label
values: ['logging=promtail'] values: ['logging=promtail']
relabel_configs: relabel_configs:
# Use docker-compose service name as the "service" label
- source_labels: ['__meta_docker_container_label_com_docker_compose_service'] - source_labels: ['__meta_docker_container_label_com_docker_compose_service']
target_label: service target_label: service
# Keep container name for context
- source_labels: ['__meta_docker_container_name'] - source_labels: ['__meta_docker_container_name']
regex: '/?(.*)' regex: '/?(.*)'
replacement: '${1}' replacement: '${1}'
target_label: container target_label: container
# Log stream (stdout / stderr)
- source_labels: ['__meta_docker_container_log_stream'] - source_labels: ['__meta_docker_container_log_stream']
target_label: stream target_label: stream
pipeline_stages: pipeline_stages:
# Drop entries older than 15 min to avoid replaying full container log history
- drop: - drop:
older_than: 15m older_than: 15m
drop_counter_reason: entry_too_old drop_counter_reason: entry_too_old
# Drop noisy health-check / ping lines
- drop: - drop:
expression: 'GET /(health|metrics|minio/health)' expression: 'GET /(health|metrics|minio/health)'
# Try to parse JSON (NestJS/pino output)
- json: - json:
expressions: expressions:
level: level level: level
@ -55,12 +45,10 @@ scrape_configs:
context: context context: context
reqId: reqId reqId: reqId
# Promote parsed fields as Loki labels
- labels: - labels:
level: level:
context: context:
# Map pino numeric levels to strings
- template: - template:
source: level source: level
template: >- template: >-