fix: Gitea Traefik routing and connection pool optimization
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
- Remove middleware reference from Gitea Traefik labels (caused routing issues) - Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s) - Add explicit service reference in Traefik labels - Fix intermittent 504 timeouts by improving PostgreSQL connection handling Fixes Gitea unreachability via git.michaelschiemer.de
This commit is contained in:
309
src/Framework/ExceptionHandling/Audit/ExceptionAuditLogger.php
Normal file
309
src/Framework/ExceptionHandling/Audit/ExceptionAuditLogger.php
Normal file
@@ -0,0 +1,309 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling\Audit;
|
||||
|
||||
use App\Framework\Audit\AuditLogger;
|
||||
use App\Framework\Audit\ValueObjects\AuditEntry;
|
||||
use App\Framework\Audit\ValueObjects\AuditableAction;
|
||||
use App\Framework\DateTime\Clock;
|
||||
use App\Framework\ExceptionHandling\Context\ExceptionContextData;
|
||||
use App\Framework\ExceptionHandling\Context\ExceptionContextProvider;
|
||||
use App\Framework\Http\IpAddress;
|
||||
use App\Framework\UserAgent\UserAgent;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Exception Audit Logger
|
||||
*
|
||||
* Converts exceptions to audit entries for compliance and tracking.
|
||||
* Only logs exceptions marked as auditable via ExceptionContextData.
|
||||
*
|
||||
* PHP 8.5+ with external context management via WeakMap.
|
||||
*/
|
||||
final readonly class ExceptionAuditLogger
|
||||
{
|
||||
public function __construct(
|
||||
private AuditLogger $auditLogger,
|
||||
private Clock $clock,
|
||||
private ?ExceptionContextProvider $contextProvider = null
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Log exception as audit entry if auditable
|
||||
*
|
||||
* @param Throwable $exception Exception to log
|
||||
* @param ExceptionContextData|null $context Optional context (if not provided, will be retrieved from provider)
|
||||
*/
|
||||
public function logIfAuditable(Throwable $exception, ?ExceptionContextData $context = null): void
|
||||
{
|
||||
try {
|
||||
// Get context from provider if not provided
|
||||
$context = $context ?? $this->getContext($exception);
|
||||
|
||||
// Skip if not auditable
|
||||
if (!$this->isAuditable($context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert exception to audit entry
|
||||
$auditEntry = $this->convertToAuditEntry($exception, $context);
|
||||
|
||||
// Log to audit system
|
||||
$this->auditLogger->log($auditEntry);
|
||||
} catch (Throwable $e) {
|
||||
// Audit logging should never break exception handling
|
||||
// Log error but don't throw
|
||||
error_log("Failed to log exception to audit: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if exception is auditable
|
||||
*
|
||||
* @param ExceptionContextData|null $context Exception context
|
||||
* @return bool True if exception should be audited
|
||||
*/
|
||||
private function isAuditable(?ExceptionContextData $context): bool
|
||||
{
|
||||
if ($context === null) {
|
||||
// Default: log all exceptions if no context provided
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check auditable flag (defaults to true)
|
||||
return $context->auditable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert exception to audit entry
|
||||
*
|
||||
* @param Throwable $exception Exception to convert
|
||||
* @param ExceptionContextData $context Exception context
|
||||
* @return AuditEntry Audit entry value object
|
||||
*/
|
||||
private function convertToAuditEntry(Throwable $exception, ExceptionContextData $context): AuditEntry
|
||||
{
|
||||
// Determine audit action from exception type and context
|
||||
$action = $this->determineAuditAction($exception, $context);
|
||||
|
||||
// Determine entity type from exception class or context
|
||||
$entityType = $this->determineEntityType($exception, $context);
|
||||
|
||||
// Extract entity ID from context data
|
||||
$entityId = $this->extractEntityId($context);
|
||||
|
||||
// Build metadata from exception and context
|
||||
$metadata = $this->buildMetadata($exception, $context);
|
||||
|
||||
// Extract IP address (already Value Object or string)
|
||||
$ipAddress = $context->clientIp instanceof IpAddress
|
||||
? $context->clientIp
|
||||
: ($context->clientIp !== null && IpAddress::isValid($context->clientIp)
|
||||
? IpAddress::from($context->clientIp)
|
||||
: null);
|
||||
|
||||
// Extract user agent (already Value Object or string)
|
||||
$userAgent = $context->userAgent instanceof UserAgent
|
||||
? $context->userAgent
|
||||
: ($context->userAgent !== null
|
||||
? UserAgent::fromString($context->userAgent)
|
||||
: null);
|
||||
|
||||
// Create audit entry as failed operation
|
||||
return AuditEntry::failed(
|
||||
clock: $this->clock,
|
||||
action: $action,
|
||||
entityType: $entityType,
|
||||
entityId: $entityId,
|
||||
errorMessage: $exception->getMessage(),
|
||||
userId: $context->userId,
|
||||
ipAddress: $ipAddress,
|
||||
userAgent: $userAgent,
|
||||
metadata: $metadata
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine audit action from exception and context
|
||||
*
|
||||
* @param Throwable $exception Exception
|
||||
* @param ExceptionContextData $context Context
|
||||
* @return AuditableAction Audit action
|
||||
*/
|
||||
private function determineAuditAction(Throwable $exception, ExceptionContextData $context): AuditableAction
|
||||
{
|
||||
// Check if action is specified in metadata
|
||||
if (isset($context->metadata['audit_action']) && is_string($context->metadata['audit_action'])) {
|
||||
$actionValue = $context->metadata['audit_action'];
|
||||
// Try to find matching enum case
|
||||
foreach (AuditableAction::cases() as $case) {
|
||||
if ($case->value === $actionValue) {
|
||||
return $case;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine from operation name
|
||||
if ($context->operation !== null) {
|
||||
$operation = $context->operation;
|
||||
if (str_contains($operation, 'create')) {
|
||||
return AuditableAction::CREATE;
|
||||
}
|
||||
if (str_contains($operation, 'update')) {
|
||||
return AuditableAction::UPDATE;
|
||||
}
|
||||
if (str_contains($operation, 'delete')) {
|
||||
return AuditableAction::DELETE;
|
||||
}
|
||||
if (str_contains($operation, 'read') || str_contains($operation, 'get')) {
|
||||
return AuditableAction::READ;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine from exception type
|
||||
$exceptionClass = $exception::class;
|
||||
if (str_contains($exceptionClass, 'Security') || str_contains($exceptionClass, 'Auth')) {
|
||||
return AuditableAction::SECURITY_VIOLATION;
|
||||
}
|
||||
|
||||
// Default: custom action
|
||||
return AuditableAction::CUSTOM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine entity type from exception and context
|
||||
*
|
||||
* @param Throwable $exception Exception
|
||||
* @param ExceptionContextData $context Context
|
||||
* @return string Entity type
|
||||
*/
|
||||
private function determineEntityType(Throwable $exception, ExceptionContextData $context): string
|
||||
{
|
||||
// Check if entity type is specified in metadata
|
||||
if (isset($context->metadata['entity_type']) && is_string($context->metadata['entity_type'])) {
|
||||
return $context->metadata['entity_type'];
|
||||
}
|
||||
|
||||
// Try to extract from component
|
||||
if ($context->component !== null) {
|
||||
// Remove common suffixes
|
||||
$component = $context->component;
|
||||
$component = preg_replace('/Service$|Repository$|Manager$|Handler$/', '', $component);
|
||||
return strtolower($component);
|
||||
}
|
||||
|
||||
// Extract from exception class name
|
||||
$className = $exception::class;
|
||||
$parts = explode('\\', $className);
|
||||
$shortName = end($parts);
|
||||
|
||||
// Remove "Exception" suffix
|
||||
$entityType = preg_replace('/Exception$/', '', $shortName);
|
||||
|
||||
return strtolower($entityType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract entity ID from context data
|
||||
*
|
||||
* @param ExceptionContextData $context Context
|
||||
* @return string|null Entity ID
|
||||
*/
|
||||
private function extractEntityId(ExceptionContextData $context): ?string
|
||||
{
|
||||
// Check common entity ID keys
|
||||
$commonKeys = ['entity_id', 'id', 'user_id', 'order_id', 'account_id', 'resource_id'];
|
||||
|
||||
foreach ($commonKeys as $key) {
|
||||
if (isset($context->data[$key])) {
|
||||
$value = $context->data[$key];
|
||||
if (is_string($value) || is_numeric($value)) {
|
||||
return (string) $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build metadata from exception and context
|
||||
*
|
||||
* @param Throwable $exception Exception
|
||||
* @param ExceptionContextData $context Context
|
||||
* @return array<string, mixed> Metadata
|
||||
*/
|
||||
private function buildMetadata(Throwable $exception, ExceptionContextData $context): array
|
||||
{
|
||||
$metadata = [
|
||||
'exception_class' => $exception::class,
|
||||
'exception_code' => $exception->getCode(),
|
||||
'exception_file' => $exception->getFile(),
|
||||
'exception_line' => $exception->getLine(),
|
||||
];
|
||||
|
||||
// Add context data
|
||||
if (!empty($context->data)) {
|
||||
$metadata['context_data'] = $context->data;
|
||||
}
|
||||
|
||||
// Add operation and component
|
||||
if ($context->operation !== null) {
|
||||
$metadata['operation'] = $context->operation;
|
||||
}
|
||||
if ($context->component !== null) {
|
||||
$metadata['component'] = $context->component;
|
||||
}
|
||||
|
||||
// Add request/session IDs
|
||||
if ($context->requestId !== null) {
|
||||
$metadata['request_id'] = $context->requestId;
|
||||
}
|
||||
if ($context->sessionId !== null) {
|
||||
$metadata['session_id'] = $context->sessionId;
|
||||
}
|
||||
|
||||
// Add tags
|
||||
if (!empty($context->tags)) {
|
||||
$metadata['tags'] = $context->tags;
|
||||
}
|
||||
|
||||
// Add previous exception if exists
|
||||
if ($exception->getPrevious() !== null) {
|
||||
$previous = $exception->getPrevious();
|
||||
$metadata['previous_exception'] = [
|
||||
'class' => $previous::class,
|
||||
'message' => $previous->getMessage(),
|
||||
];
|
||||
}
|
||||
|
||||
// Merge with existing metadata (but exclude internal fields)
|
||||
$excludeKeys = ['auditable', 'audit_action', 'entity_type'];
|
||||
foreach ($context->metadata as $key => $value) {
|
||||
if (!in_array($key, $excludeKeys, true)) {
|
||||
$metadata[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get context from provider
|
||||
*
|
||||
* @param Throwable $exception Exception
|
||||
* @return ExceptionContextData|null Context or null if not found
|
||||
*/
|
||||
private function getContext(Throwable $exception): ?ExceptionContextData
|
||||
{
|
||||
if ($this->contextProvider === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->contextProvider->get($exception);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user