Implémentation complète de la stack d'observabilité pour le monitoring de la plateforme multi-tenant Classeo. ## Error Tracking (GlitchTip) - Intégration Sentry SDK avec GlitchTip auto-hébergé - Scrubber PII avant envoi (RGPD: emails, tokens JWT, NIR français) - Contexte enrichi: tenant_id, user_id, correlation_id - Configuration backend (sentry.yaml) et frontend (sentry.ts) ## Metrics (Prometheus) - Endpoint /metrics avec restriction IP en production - Métriques HTTP: requests_total, request_duration_seconds (histogramme) - Métriques sécurité: login_failures_total par tenant - Métriques santé: health_check_status (postgres, redis, rabbitmq) - Storage Redis pour persistance entre requêtes ## Logs (Loki) - Processors Monolog: CorrelationIdLogProcessor, PiiScrubberLogProcessor - Détection PII: emails, téléphones FR, tokens JWT, NIR français - Labels structurés: tenant_id, correlation_id, level ## Dashboards (Grafana) - Dashboard principal: latence P50/P95/P99, error rate, RPS - Dashboard par tenant: métriques isolées par sous-domaine - Dashboard infrastructure: santé postgres/redis/rabbitmq - Datasources avec UIDs fixes pour portabilité ## Alertes (Alertmanager) - HighApiLatencyP95/P99: SLA monitoring (200ms/500ms) - HighErrorRate: error rate > 1% pendant 2 min - ExcessiveLoginFailures: détection brute force - ApplicationUnhealthy: health check failures ## Infrastructure - InfrastructureHealthChecker: service partagé (DRY) - HealthCheckController: endpoint /health pour load balancers - Pre-push hook: make ci && make e2e avant push
161 lines
4.6 KiB
PHP
161 lines
4.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Unit\Shared\Infrastructure\Monitoring;
|
|
|
|
use App\Shared\Infrastructure\Monitoring\HealthCheckController;
|
|
use App\Shared\Infrastructure\Monitoring\InfrastructureHealthCheckerInterface;
|
|
use PHPUnit\Framework\Attributes\CoversClass;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
/**
|
|
* Stub for InfrastructureHealthCheckerInterface.
|
|
*/
|
|
final class InfrastructureHealthCheckerStub implements InfrastructureHealthCheckerInterface
|
|
{
|
|
/**
|
|
* @param array{postgres: bool, redis: bool, rabbitmq: bool} $checks
|
|
*/
|
|
public function __construct(
|
|
private readonly array $checks = ['postgres' => true, 'redis' => true, 'rabbitmq' => true],
|
|
) {
|
|
}
|
|
|
|
public function checkPostgres(): bool
|
|
{
|
|
return $this->checks['postgres'];
|
|
}
|
|
|
|
public function checkRedis(): bool
|
|
{
|
|
return $this->checks['redis'];
|
|
}
|
|
|
|
public function checkRabbitMQ(): bool
|
|
{
|
|
return $this->checks['rabbitmq'];
|
|
}
|
|
|
|
public function checkAll(): array
|
|
{
|
|
return $this->checks;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @see Story 1.8 - T7: Health Check Endpoint
|
|
*/
|
|
#[CoversClass(HealthCheckController::class)]
|
|
final class HealthCheckControllerTest extends TestCase
|
|
{
|
|
private function createController(
|
|
?InfrastructureHealthCheckerInterface $healthChecker = null,
|
|
): HealthCheckController {
|
|
$healthChecker ??= new InfrastructureHealthCheckerStub();
|
|
|
|
return new HealthCheckController($healthChecker);
|
|
}
|
|
|
|
#[Test]
|
|
public function itReturnsHealthyWhenAllServicesAreUp(): void
|
|
{
|
|
$controller = $this->createController();
|
|
|
|
$response = $controller();
|
|
|
|
self::assertSame(Response::HTTP_OK, $response->getStatusCode());
|
|
|
|
$data = json_decode($response->getContent(), true);
|
|
self::assertSame('healthy', $data['status']);
|
|
self::assertTrue($data['checks']['postgres']);
|
|
self::assertTrue($data['checks']['redis']);
|
|
self::assertTrue($data['checks']['rabbitmq']);
|
|
}
|
|
|
|
#[Test]
|
|
public function itReturnsUnhealthyWhenPostgresIsDown(): void
|
|
{
|
|
$checker = new InfrastructureHealthCheckerStub([
|
|
'postgres' => false,
|
|
'redis' => true,
|
|
'rabbitmq' => true,
|
|
]);
|
|
$controller = $this->createController($checker);
|
|
|
|
$response = $controller();
|
|
|
|
self::assertSame(Response::HTTP_SERVICE_UNAVAILABLE, $response->getStatusCode());
|
|
|
|
$data = json_decode($response->getContent(), true);
|
|
self::assertSame('unhealthy', $data['status']);
|
|
self::assertFalse($data['checks']['postgres']);
|
|
}
|
|
|
|
#[Test]
|
|
public function itIncludesTimestampInResponse(): void
|
|
{
|
|
$controller = $this->createController();
|
|
|
|
$response = $controller();
|
|
$data = json_decode($response->getContent(), true);
|
|
|
|
self::assertArrayHasKey('timestamp', $data);
|
|
self::assertMatchesRegularExpression(
|
|
'/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+[+-]\d{2}:\d{2}$/',
|
|
$data['timestamp'],
|
|
);
|
|
}
|
|
|
|
#[Test]
|
|
public function itReturnsAllServiceChecks(): void
|
|
{
|
|
$controller = $this->createController();
|
|
|
|
$response = $controller();
|
|
$data = json_decode($response->getContent(), true);
|
|
|
|
self::assertArrayHasKey('postgres', $data['checks']);
|
|
self::assertArrayHasKey('redis', $data['checks']);
|
|
self::assertArrayHasKey('rabbitmq', $data['checks']);
|
|
}
|
|
|
|
#[Test]
|
|
public function itReturnsUnhealthyWhenRabbitmqIsDown(): void
|
|
{
|
|
$checker = new InfrastructureHealthCheckerStub([
|
|
'postgres' => true,
|
|
'redis' => true,
|
|
'rabbitmq' => false,
|
|
]);
|
|
$controller = $this->createController($checker);
|
|
|
|
$response = $controller();
|
|
|
|
self::assertSame(Response::HTTP_SERVICE_UNAVAILABLE, $response->getStatusCode());
|
|
|
|
$data = json_decode($response->getContent(), true);
|
|
self::assertFalse($data['checks']['rabbitmq']);
|
|
}
|
|
|
|
#[Test]
|
|
public function itReturnsUnhealthyWhenRedisIsDown(): void
|
|
{
|
|
$checker = new InfrastructureHealthCheckerStub([
|
|
'postgres' => true,
|
|
'redis' => false,
|
|
'rabbitmq' => true,
|
|
]);
|
|
$controller = $this->createController($checker);
|
|
|
|
$response = $controller();
|
|
|
|
self::assertSame(Response::HTTP_SERVICE_UNAVAILABLE, $response->getStatusCode());
|
|
|
|
$data = json_decode($response->getContent(), true);
|
|
self::assertFalse($data['checks']['redis']);
|
|
}
|
|
}
|