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

- 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:
2025-11-09 14:46:15 +01:00
parent 85c369e846
commit 36ef2a1e2c
1366 changed files with 104925 additions and 28719 deletions

View File

@@ -0,0 +1,302 @@
<?php
declare(strict_types=1);
namespace App\Framework\ExceptionHandling\Context;
use App\Framework\Cache\Cache;
use App\Framework\ExceptionHandling\Scope\ErrorScope;
use App\Framework\Http\IpAddress;
use App\Framework\Http\Request;
use App\Framework\Http\Session\SessionId;
use App\Framework\UserAgent\UserAgent;
/**
* Exception Context Builder
*
* Automatically collects context from various sources:
* - ErrorScope (if available)
* - HTTP Request (if in HTTP context)
* - Session (if available)
* - Authentication (User ID)
* - System information (optional)
*
* Provides a simple API for automatic context collection and reduces boilerplate.
*/
final readonly class ExceptionContextBuilder
{
public function __construct(
private ?ErrorScope $errorScope = null,
private ?ExceptionContextCache $contextCache = null
) {
}
/**
* Build context from current environment
*
* Automatically collects context from ErrorScope, HTTP Request, Session, etc.
*/
public function build(?ExceptionContextData $baseContext = null): ExceptionContextData
{
$context = $baseContext ?? ExceptionContextData::empty();
// First, enrich from ErrorScope if available
if ($this->errorScope !== null) {
$scopeContext = $this->errorScope->current();
if ($scopeContext !== null) {
$context = $this->enrichFromScope($context, $scopeContext);
}
}
return $context;
}
/**
* Build context from HTTP request
*
* Extracts context from HTTP request, session, and authentication.
* Uses cache if available to improve performance.
*/
public function buildFromRequest(Request $request, ?ExceptionContextData $baseContext = null): ExceptionContextData
{
// Try to get from cache first
if ($this->contextCache !== null) {
$requestId = $request instanceof \App\Framework\Http\HttpRequest ? $request->id->toString() : null;
$sessionId = property_exists($request, 'session') && $request->session !== null
? $request->session->id->toString()
: null;
$userId = $this->extractUserId($request);
$cached = $this->contextCache->get($requestId, $sessionId, $userId);
if ($cached !== null) {
// Merge with base context if provided
if ($baseContext !== null) {
return $this->mergeContexts($cached, $baseContext);
}
return $cached;
}
}
$context = $baseContext ?? ExceptionContextData::empty();
// Extract request ID
if ($request instanceof \App\Framework\Http\HttpRequest) {
$context = $context->withRequestId($request->id->toString());
}
// Extract IP address
$ipAddress = $request->server->getRemoteAddr();
if ($ipAddress !== null) {
$ipValue = $ipAddress->value;
if (is_string($ipValue) && IpAddress::isValid($ipValue)) {
$context = $context->withClientIp(IpAddress::from($ipValue));
} else {
$context = $context->withClientIp($ipValue);
}
}
// Extract user agent
$userAgent = $request->server->getUserAgent();
if ($userAgent !== null) {
$userAgentValue = $userAgent->value;
if (is_string($userAgentValue)) {
$context = $context->withUserAgent(UserAgent::fromString($userAgentValue));
} else {
$context = $context->withUserAgent($userAgentValue);
}
}
// Extract session ID
if (property_exists($request, 'session') && $request->session !== null) {
$sessionId = $request->session->id->toString();
try {
$context = $context->withSessionId(SessionId::fromString($sessionId));
} catch (\InvalidArgumentException) {
// If SessionId validation fails, keep as string for backward compatibility
$context = $context->withSessionId($sessionId);
}
}
// Extract user ID (if authenticated)
$userId = $this->extractUserId($request);
if ($userId !== null) {
$context = $context->withUserId($userId);
}
// Add HTTP-specific tags
$context = $context->withTags('http', 'web');
// Enrich from ErrorScope if available (may override some values)
if ($this->errorScope !== null) {
$scopeContext = $this->errorScope->current();
if ($scopeContext !== null) {
$context = $this->enrichFromScope($context, $scopeContext);
}
}
// Cache context if cache is available
if ($this->contextCache !== null) {
$requestId = $request instanceof \App\Framework\Http\HttpRequest ? $request->id->toString() : null;
$sessionId = property_exists($request, 'session') && $request->session !== null
? $request->session->id->toString()
: null;
$userId = $this->extractUserId($request);
$this->contextCache->put($context, $requestId, $sessionId, $userId);
}
return $context;
}
/**
* Merge two contexts (base takes precedence)
*/
private function mergeContexts(ExceptionContextData $cached, ExceptionContextData $base): ExceptionContextData
{
$merged = $cached;
// Override with base context values if present
if ($base->operation !== null) {
$merged = $merged->withOperation($base->operation, $base->component);
}
if ($base->component !== null && $merged->component === null) {
$merged = $merged->withOperation($merged->operation ?? '', $base->component);
}
if (!empty($base->data)) {
$merged = $merged->addData($base->data);
}
if (!empty($base->debug)) {
$merged = $merged->addDebug($base->debug);
}
if (!empty($base->metadata)) {
$merged = $merged->addMetadata($base->metadata);
}
if ($base->userId !== null) {
$merged = $merged->withUserId($base->userId);
}
if ($base->requestId !== null) {
$merged = $merged->withRequestId($base->requestId);
}
if ($base->sessionId !== null) {
$merged = $merged->withSessionId($base->sessionId);
}
if ($base->clientIp !== null) {
$merged = $merged->withClientIp($base->clientIp);
}
if ($base->userAgent !== null) {
$merged = $merged->withUserAgent($base->userAgent);
}
if (!empty($base->tags)) {
$merged = $merged->withTags(...array_merge($merged->tags, $base->tags));
}
return $merged;
}
/**
* Enrich context from ErrorScope
*/
private function enrichFromScope(
ExceptionContextData $context,
\App\Framework\ExceptionHandling\Scope\ErrorScopeContext $scopeContext
): ExceptionContextData {
// Add operation/component from scope if not already set
if ($context->operation === null && $scopeContext->operation !== null) {
$context = $context->withOperation(
$scopeContext->operation,
$scopeContext->component
);
}
// Add user ID from scope if not already set
if ($context->userId === null && $scopeContext->userId !== null) {
$context = $context->withUserId($scopeContext->userId);
}
// Add request ID from scope if not already set
if ($context->requestId === null && $scopeContext->requestId !== null) {
$context = $context->withRequestId($scopeContext->requestId);
}
// Add session ID from scope if not already set
if ($context->sessionId === null && $scopeContext->sessionId !== null) {
if (is_string($scopeContext->sessionId)) {
try {
$context = $context->withSessionId(SessionId::fromString($scopeContext->sessionId));
} catch (\InvalidArgumentException) {
$context = $context->withSessionId($scopeContext->sessionId);
}
} else {
$context = $context->withSessionId($scopeContext->sessionId);
}
}
// Extract HTTP fields from scope metadata (for HTTP scopes)
if (isset($scopeContext->metadata['ip']) && $context->clientIp === null) {
$ipValue = $scopeContext->metadata['ip'];
if (is_string($ipValue) && IpAddress::isValid($ipValue)) {
$context = $context->withClientIp(IpAddress::from($ipValue));
} elseif ($ipValue instanceof IpAddress) {
$context = $context->withClientIp($ipValue);
} else {
$context = $context->withClientIp($ipValue);
}
}
if (isset($scopeContext->metadata['user_agent']) && $context->userAgent === null) {
$userAgentValue = $scopeContext->metadata['user_agent'];
if (is_string($userAgentValue)) {
$context = $context->withUserAgent(UserAgent::fromString($userAgentValue));
} elseif ($userAgentValue instanceof UserAgent) {
$context = $context->withUserAgent($userAgentValue);
} else {
$context = $context->withUserAgent($userAgentValue);
}
}
// Add scope metadata
$context = $context->addMetadata([
'scope_type' => $scopeContext->type->value,
'scope_id' => $scopeContext->scopeId,
]);
// Add scope tags
if (!empty($scopeContext->tags)) {
$context = $context->withTags(...$scopeContext->tags);
}
return $context;
}
/**
* Extract user ID from request
*/
private function extractUserId(Request $request): ?string
{
// Try to get from request attribute (set by auth middleware)
if (method_exists($request, 'getAttribute')) {
$user = $request->getAttribute('user');
if ($user !== null) {
if (is_object($user) && property_exists($user, 'id')) {
return (string) $user->id;
}
if (is_string($user)) {
return $user;
}
}
}
// Try to get from request property (if available)
if (property_exists($request, 'user') && $request->user !== null) {
if (is_object($request->user) && property_exists($request->user, 'id')) {
return (string) $request->user->id;
}
if (is_string($request->user)) {
return $request->user;
}
}
return null;
}
}

View File

@@ -0,0 +1,213 @@
<?php
declare(strict_types=1);
namespace App\Framework\ExceptionHandling\Context;
use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheItem;
use App\Framework\Cache\CacheKey;
use App\Framework\Core\ValueObjects\Duration;
/**
* Exception Context Cache
*
* Caches frequently used context data to improve performance.
* Uses cache keys based on Request-ID, Session-ID, User-ID.
*/
final readonly class ExceptionContextCache
{
private const string CACHE_PREFIX = 'exception_context:';
private const int REQUEST_CONTEXT_TTL = 600; // 10 minutes
private const int USER_CONTEXT_TTL = 1800; // 30 minutes
public function __construct(
private Cache $cache
) {
}
/**
* Get cached context for request
*
* @param string|null $requestId Request ID
* @param string|null $sessionId Session ID
* @param string|null $userId User ID
* @return ExceptionContextData|null Cached context or null if not found
*/
public function get(
?string $requestId = null,
?string $sessionId = null,
?string $userId = null
): ?ExceptionContextData {
// Try request-level cache first (most specific)
if ($requestId !== null) {
$cacheKey = $this->buildRequestCacheKey($requestId);
$cached = $this->getFromCache($cacheKey);
if ($cached !== null) {
return $cached;
}
}
// Try session-level cache
if ($sessionId !== null) {
$cacheKey = $this->buildSessionCacheKey($sessionId);
$cached = $this->getFromCache($cacheKey);
if ($cached !== null) {
return $cached;
}
}
// Try user-level cache (least specific)
if ($userId !== null) {
$cacheKey = $this->buildUserCacheKey($userId);
$cached = $this->getFromCache($cacheKey);
if ($cached !== null) {
return $cached;
}
}
return null;
}
/**
* Cache context for request
*
* @param ExceptionContextData $context Context to cache
* @param string|null $requestId Request ID
* @param string|null $sessionId Session ID
* @param string|null $userId User ID
*/
public function put(
ExceptionContextData $context,
?string $requestId = null,
?string $sessionId = null,
?string $userId = null
): void {
// Cache at request level (most specific)
if ($requestId !== null) {
$cacheKey = $this->buildRequestCacheKey($requestId);
$this->putToCache($cacheKey, $context, Duration::fromSeconds(self::REQUEST_CONTEXT_TTL));
}
// Cache at session level
if ($sessionId !== null) {
$cacheKey = $this->buildSessionCacheKey($sessionId);
$this->putToCache($cacheKey, $context, Duration::fromSeconds(self::REQUEST_CONTEXT_TTL));
}
// Cache at user level (least specific, longer TTL)
if ($userId !== null) {
$cacheKey = $this->buildUserCacheKey($userId);
// Only cache user-specific parts to avoid stale data
$userContext = ExceptionContextData::empty()
->withUserId($context->userId)
->withClientIp($context->clientIp)
->withUserAgent($context->userAgent);
$this->putToCache($cacheKey, $userContext, Duration::fromSeconds(self::USER_CONTEXT_TTL));
}
}
/**
* Invalidate cache for request
*
* Called when request context changes (e.g., user logs in/out).
*/
public function invalidateRequest(?string $requestId = null): void
{
if ($requestId !== null) {
$cacheKey = $this->buildRequestCacheKey($requestId);
$this->cache->forget($cacheKey);
}
}
/**
* Invalidate cache for session
*
* Called when session changes (e.g., session regenerated).
*/
public function invalidateSession(?string $sessionId = null): void
{
if ($sessionId !== null) {
$cacheKey = $this->buildSessionCacheKey($sessionId);
$this->cache->forget($cacheKey);
}
}
/**
* Invalidate cache for user
*
* Called when user context changes (e.g., user logs in/out).
*/
public function invalidateUser(?string $userId = null): void
{
if ($userId !== null) {
$cacheKey = $this->buildUserCacheKey($userId);
$this->cache->forget($cacheKey);
}
}
/**
* Build cache key for request
*/
private function buildRequestCacheKey(string $requestId): CacheKey
{
return CacheKey::fromString(self::CACHE_PREFIX . 'request:' . $requestId);
}
/**
* Build cache key for session
*/
private function buildSessionCacheKey(string $sessionId): CacheKey
{
return CacheKey::fromString(self::CACHE_PREFIX . 'session:' . $sessionId);
}
/**
* Build cache key for user
*/
private function buildUserCacheKey(string $userId): CacheKey
{
return CacheKey::fromString(self::CACHE_PREFIX . 'user:' . $userId);
}
/**
* Get context from cache
*/
private function getFromCache(CacheKey $cacheKey): ?ExceptionContextData
{
$result = $this->cache->get($cacheKey);
$item = $result->getFirstHit();
if ($item === null) {
return null;
}
$value = $item->value;
if ($value instanceof ExceptionContextData) {
return $value;
}
// Try to deserialize from array
if (is_array($value)) {
return ExceptionContextData::fromArray($value);
}
return null;
}
/**
* Put context to cache
*/
private function putToCache(CacheKey $cacheKey, ExceptionContextData $context, Duration $ttl): void
{
// Store as array for serialization compatibility
$cacheItem = CacheItem::fromKey(
$cacheKey,
$context->toArray(),
$ttl
);
$this->cache->set($cacheItem);
}
}

View File

@@ -4,6 +4,10 @@ declare(strict_types=1);
namespace App\Framework\ExceptionHandling\Context;
use App\Framework\Http\IpAddress;
use App\Framework\Http\RequestId;
use App\Framework\Http\Session\SessionId;
use App\Framework\UserAgent\UserAgent;
use DateTimeImmutable;
/**
@@ -26,11 +30,13 @@ final readonly class ExceptionContextData
* @param array<string, mixed> $metadata Additional metadata (tags, severity, fingerprint)
* @param DateTimeImmutable|null $occurredAt When the exception occurred
* @param string|null $userId User ID if available
* @param string|null $requestId Request ID for tracing
* @param string|null $sessionId Session ID if available
* @param string|null $clientIp Client IP address for HTTP requests
* @param string|null $userAgent User agent string for HTTP requests
* @param RequestId|string|null $requestId Request ID for tracing (Value Object or string for backward compatibility)
* @param SessionId|string|null $sessionId Session ID if available (Value Object or string for backward compatibility)
* @param IpAddress|string|null $clientIp Client IP address for HTTP requests (Value Object or string for backward compatibility)
* @param UserAgent|string|null $userAgent User agent string for HTTP requests (Value Object or string for backward compatibility)
* @param array<string> $tags Tags for categorization (e.g., ['payment', 'external_api'])
* @param bool $auditable Whether this exception should be logged to audit system
* @param string|null $auditLevel Audit level for this exception (e.g., 'ERROR', 'WARNING', 'INFO')
*/
public function __construct(
public ?string $operation = null,
@@ -40,11 +46,13 @@ final readonly class ExceptionContextData
public array $metadata = [],
?DateTimeImmutable $occurredAt = null,
public ?string $userId = null,
public ?string $requestId = null,
public ?string $sessionId = null,
public ?string $clientIp = null,
public ?string $userAgent = null,
public RequestId|string|null $requestId = null,
public SessionId|string|null $sessionId = null,
public IpAddress|string|null $clientIp = null,
public UserAgent|string|null $userAgent = null,
public array $tags = [],
public bool $auditable = true,
public ?string $auditLevel = null,
) {
$this->occurredAt ??= new DateTimeImmutable();
}
@@ -93,7 +101,9 @@ final readonly class ExceptionContextData
sessionId: $this->sessionId,
clientIp: $this->clientIp,
userAgent: $this->userAgent,
tags: $this->tags
tags: $this->tags,
auditable: $this->auditable,
auditLevel: $this->auditLevel
);
}
@@ -114,7 +124,9 @@ final readonly class ExceptionContextData
sessionId: $this->sessionId,
clientIp: $this->clientIp,
userAgent: $this->userAgent,
tags: $this->tags
tags: $this->tags,
auditable: $this->auditable,
auditLevel: $this->auditLevel
);
}
@@ -135,7 +147,9 @@ final readonly class ExceptionContextData
sessionId: $this->sessionId,
clientIp: $this->clientIp,
userAgent: $this->userAgent,
tags: $this->tags
tags: $this->tags,
auditable: $this->auditable,
auditLevel: $this->auditLevel
);
}
@@ -156,7 +170,9 @@ final readonly class ExceptionContextData
sessionId: $this->sessionId,
clientIp: $this->clientIp,
userAgent: $this->userAgent,
tags: $this->tags
tags: $this->tags,
auditable: $this->auditable,
auditLevel: $this->auditLevel
);
}
@@ -177,14 +193,18 @@ final readonly class ExceptionContextData
sessionId: $this->sessionId,
clientIp: $this->clientIp,
userAgent: $this->userAgent,
tags: $this->tags
tags: $this->tags,
auditable: $this->auditable,
auditLevel: $this->auditLevel
);
}
/**
* Add request ID
*
* @param RequestId|string $requestId Request ID (Value Object or string for backward compatibility)
*/
public function withRequestId(string $requestId): self
public function withRequestId(RequestId|string $requestId): self
{
return new self(
operation: $this->operation,
@@ -198,14 +218,18 @@ final readonly class ExceptionContextData
sessionId: $this->sessionId,
clientIp: $this->clientIp,
userAgent: $this->userAgent,
tags: $this->tags
tags: $this->tags,
auditable: $this->auditable,
auditLevel: $this->auditLevel
);
}
/**
* Add session ID
*
* @param SessionId|string $sessionId Session ID (Value Object or string for backward compatibility)
*/
public function withSessionId(string $sessionId): self
public function withSessionId(SessionId|string $sessionId): self
{
return new self(
operation: $this->operation,
@@ -219,14 +243,18 @@ final readonly class ExceptionContextData
sessionId: $sessionId,
clientIp: $this->clientIp,
userAgent: $this->userAgent,
tags: $this->tags
tags: $this->tags,
auditable: $this->auditable,
auditLevel: $this->auditLevel
);
}
/**
* Add client IP
*
* @param IpAddress|string $clientIp Client IP (Value Object or string for backward compatibility)
*/
public function withClientIp(string $clientIp): self
public function withClientIp(IpAddress|string $clientIp): self
{
return new self(
operation: $this->operation,
@@ -240,14 +268,18 @@ final readonly class ExceptionContextData
sessionId: $this->sessionId,
clientIp: $clientIp,
userAgent: $this->userAgent,
tags: $this->tags
tags: $this->tags,
auditable: $this->auditable,
auditLevel: $this->auditLevel
);
}
/**
* Add user agent
*
* @param UserAgent|string $userAgent User agent (Value Object or string for backward compatibility)
*/
public function withUserAgent(string $userAgent): self
public function withUserAgent(UserAgent|string $userAgent): self
{
return new self(
operation: $this->operation,
@@ -261,7 +293,9 @@ final readonly class ExceptionContextData
sessionId: $this->sessionId,
clientIp: $this->clientIp,
userAgent: $userAgent,
tags: $this->tags
tags: $this->tags,
auditable: $this->auditable,
auditLevel: $this->auditLevel
);
}
@@ -282,7 +316,55 @@ final readonly class ExceptionContextData
sessionId: $this->sessionId,
clientIp: $this->clientIp,
userAgent: $this->userAgent,
tags: array_merge($this->tags, $tags)
tags: array_merge($this->tags, $tags),
auditable: $this->auditable,
auditLevel: $this->auditLevel
);
}
/**
* Set auditable flag
*/
public function withAuditable(bool $auditable): self
{
return new self(
operation: $this->operation,
component: $this->component,
data: $this->data,
debug: $this->debug,
metadata: $this->metadata,
occurredAt: $this->occurredAt,
userId: $this->userId,
requestId: $this->requestId,
sessionId: $this->sessionId,
clientIp: $this->clientIp,
userAgent: $this->userAgent,
tags: $this->tags,
auditable: $auditable,
auditLevel: $this->auditLevel
);
}
/**
* Set audit level
*/
public function withAuditLevel(string $auditLevel): self
{
return new self(
operation: $this->operation,
component: $this->component,
data: $this->data,
debug: $this->debug,
metadata: $this->metadata,
occurredAt: $this->occurredAt,
userId: $this->userId,
requestId: $this->requestId,
sessionId: $this->sessionId,
clientIp: $this->clientIp,
userAgent: $this->userAgent,
tags: $this->tags,
auditable: $this->auditable,
auditLevel: $auditLevel
);
}
@@ -299,11 +381,47 @@ final readonly class ExceptionContextData
'metadata' => $this->metadata,
'occurred_at' => $this->occurredAt?->format('Y-m-d H:i:s.u'),
'user_id' => $this->userId,
'request_id' => $this->requestId,
'session_id' => $this->sessionId,
'client_ip' => $this->clientIp,
'user_agent' => $this->userAgent,
'request_id' => $this->requestId instanceof RequestId ? $this->requestId->toString() : $this->requestId,
'session_id' => $this->sessionId instanceof SessionId ? $this->sessionId->toString() : $this->sessionId,
'client_ip' => $this->clientIp instanceof IpAddress ? $this->clientIp->value : $this->clientIp,
'user_agent' => $this->userAgent instanceof UserAgent ? $this->userAgent->value : $this->userAgent,
'tags' => $this->tags,
'auditable' => $this->auditable,
'audit_level' => $this->auditLevel,
];
}
/**
* Create from array (deserialization)
*
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
$occurredAt = null;
if (isset($data['occurred_at']) && is_string($data['occurred_at'])) {
try {
$occurredAt = new DateTimeImmutable($data['occurred_at']);
} catch (\Exception) {
// Ignore invalid date
}
}
return new self(
operation: $data['operation'] ?? null,
component: $data['component'] ?? null,
data: $data['data'] ?? [],
debug: $data['debug'] ?? [],
metadata: $data['metadata'] ?? [],
occurredAt: $occurredAt,
userId: $data['user_id'] ?? null,
requestId: $data['request_id'] ?? null,
sessionId: $data['session_id'] ?? null,
clientIp: $data['client_ip'] ?? null,
userAgent: $data['user_agent'] ?? null,
tags: $data['tags'] ?? [],
auditable: $data['auditable'] ?? true,
auditLevel: $data['audit_level'] ?? null
);
}
}

View File

@@ -13,29 +13,19 @@ use WeakMap;
* Context is automatically cleaned up when the exception is garbage collected.
*
* PHP 8.5+ WeakMap-based implementation - no memory leaks possible.
*
* Should be registered as singleton in DI container for consistent context across application.
*/
final class ExceptionContextProvider
{
/** @var WeakMap<\Throwable, ExceptionContextData> */
private WeakMap $contexts;
private static ?self $instance = null;
private function __construct()
public function __construct()
{
$this->contexts = new WeakMap();
}
/**
* Get singleton instance
*
* Singleton pattern ensures consistent context across the application
*/
public static function instance(): self
{
return self::$instance ??= new self();
}
/**
* Attach context to exception
*