Files
michaelschiemer/src/Framework/ApiGateway/ApiGateway.php
Michael Schiemer 36ef2a1e2c
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
fix: Gitea Traefik routing and connection pool optimization
- 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
2025-11-09 14:46:15 +01:00

294 lines
10 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\ApiGateway;
use App\Framework\ApiGateway\Exceptions\ApiGatewayException;
use App\Framework\ApiGateway\Metrics\ApiMetrics;
use App\Framework\CircuitBreaker\CircuitBreaker;
use App\Framework\CircuitBreaker\CircuitBreakerConfig;
use App\Framework\CircuitBreaker\CircuitBreakerManager;
use App\Framework\CircuitBreaker\Exceptions\CircuitBreakerException;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\HttpClient\ClientOptions;
use App\Framework\HttpClient\ClientRequest;
use App\Framework\HttpClient\ClientResponse;
use App\Framework\HttpClient\HttpClient;
use App\Framework\Logging\Logger;
use App\Framework\Performance\OperationTracker;
use App\Framework\Performance\PerformanceCategory;
use App\Framework\Retry\RetryStrategy;
use Throwable;
/**
* Central API Gateway for all outbound API calls
*
* Provides cross-cutting concerns:
* - Circuit breaker pattern for resilience
* - Retry strategies for transient failures
* - Centralized logging and metrics
* - Request/response transformation
*
* Example usage:
*
* $request = new SendEmailApiRequest($email, $template);
* $response = $apiGateway->send($request);
*/
final readonly class ApiGateway
{
public function __construct(
private HttpClient $httpClient,
private CircuitBreakerManager $circuitBreakerManager,
private ApiMetrics $metrics,
private OperationTracker $operationTracker,
private ?Logger $logger = null
) {
}
/**
* Send an API request through the gateway
*
* @param ApiRequest $request The API request to send
* @return ClientResponse The response from the API
* @throws ApiGatewayException If the request fails after all retries
* @throws CircuitBreakerException If the circuit breaker is open
*/
public function send(ApiRequest $request): ClientResponse
{
$service = $request->getEndpoint()->getServiceIdentifier();
$requestName = $request->getRequestName();
// Start performance tracking
$operationId = "api_gateway.{$service}.{$requestName}." . uniqid();
$snapshot = $this->operationTracker->startOperation(
operationId: $operationId,
category: PerformanceCategory::API,
contextData: [
'service' => $service,
'request_name' => $requestName,
'method' => $request->getMethod()->value,
'endpoint' => $request->getEndpoint()->toString(),
]
);
$retryAttempts = 0;
$circuitBreakerTriggered = false;
// Log request start
$this->logger?->info("[ApiGateway] Sending request", [
'operation_id' => $operationId,
'service' => $service,
'request_name' => $requestName,
'method' => $request->getMethod()->value,
'endpoint' => $request->getEndpoint()->toString(),
]);
// Get circuit breaker for this service
$circuitBreaker = $this->circuitBreakerManager->getCircuitBreaker($service);
$circuitBreakerConfig = CircuitBreakerConfig::forHttpClient();
// Check circuit breaker status (throws if open)
try {
$circuitBreaker->check($service, $circuitBreakerConfig);
} catch (CircuitBreakerException $e) {
$circuitBreakerTriggered = true;
$this->logger?->warning("[ApiGateway] Circuit breaker open", [
'operation_id' => $operationId,
'service' => $service,
'request_name' => $requestName,
]);
// Record failure and metrics
$completedSnapshot = $this->operationTracker->failOperation($operationId, $e);
if ($completedSnapshot !== null) {
$this->recordMetrics($service, $requestName, $completedSnapshot, false, 0, true);
}
throw $e;
}
// Convert ApiRequest to ClientRequest
$clientRequest = $this->buildClientRequest($request);
// Execute with retry strategy if configured
$retryStrategy = $request->getRetryStrategy();
try {
if ($retryStrategy !== null) {
$result = $this->sendWithRetry($clientRequest, $retryStrategy, $service, $requestName, $operationId);
$response = $result['response'];
$retryAttempts = $result['attempts'];
} else {
$response = $this->httpClient->send($clientRequest);
}
// Record success with circuit breaker
$circuitBreaker->recordSuccess($service, $circuitBreakerConfig);
$this->logger?->info("[ApiGateway] Request successful", [
'operation_id' => $operationId,
'service' => $service,
'request_name' => $requestName,
'status' => $response->status->value,
'retry_attempts' => $retryAttempts,
]);
// Complete operation tracking and record metrics
$completedSnapshot = $this->operationTracker->completeOperation($operationId);
if ($completedSnapshot !== null) {
$this->recordMetrics($service, $requestName, $completedSnapshot, true, $retryAttempts, false);
}
return $response;
} catch (Throwable $exception) {
// Record failure with circuit breaker
$circuitBreaker->recordFailure($service, $exception, $circuitBreakerConfig);
$this->logger?->error("[ApiGateway] Request failed", [
'operation_id' => $operationId,
'service' => $service,
'request_name' => $requestName,
'error' => $exception->getMessage(),
'exception_class' => get_class($exception),
'retry_attempts' => $retryAttempts,
]);
// Record failure and metrics
$completedSnapshot = $this->operationTracker->failOperation($operationId, $exception);
if ($completedSnapshot !== null) {
$this->recordMetrics($service, $requestName, $completedSnapshot, false, $retryAttempts, $circuitBreakerTriggered);
}
throw new ApiGatewayException(
"API request failed for {$requestName}",
0,
$exception
);
}
}
/**
* Build ClientRequest from ApiRequest
*/
private function buildClientRequest(ApiRequest $request): ClientRequest
{
// Convert timeout Duration to seconds for ClientOptions
$timeoutSeconds = (int) $request->getTimeout()->toSeconds();
$options = new ClientOptions(
timeout: $timeoutSeconds,
connectTimeout: min(3, $timeoutSeconds), // Connect timeout max 3s or total timeout
);
// Add authentication if present
if ($request instanceof HasAuth) {
$options = $options->with(['auth' => $request->getAuth()]);
}
// Use factory method for JSON requests if payload is present
if ($request instanceof HasPayload) {
return ClientRequest::json(
method: $request->getMethod(),
url: $request->getEndpoint()->toString(),
data: $request->getPayload(),
options: $options
)->with([
'headers' => $request->getHeaders(),
]);
}
// Simple request without body
return new ClientRequest(
method: $request->getMethod(),
url: $request->getEndpoint()->toString(),
headers: $request->getHeaders(),
body: '',
options: $options
);
}
/**
* Send request with retry strategy
*
* @return array{response: ClientResponse, attempts: int}
*/
private function sendWithRetry(
ClientRequest $request,
RetryStrategy $retryStrategy,
string $service,
string $requestName,
string $operationId
): array {
$attempt = 0;
while (true) {
try {
if ($attempt > 0) {
$this->logger?->info("[ApiGateway] Retry attempt", [
'operation_id' => $operationId,
'service' => $service,
'request_name' => $requestName,
'attempt' => $attempt,
]);
}
$response = $this->httpClient->send($request);
return [
'response' => $response,
'attempts' => $attempt,
];
} catch (Throwable $exception) {
$attempt++;
if (!$retryStrategy->shouldRetry($exception, $attempt)) {
$this->logger?->warning("[ApiGateway] Retry limit reached", [
'operation_id' => $operationId,
'service' => $service,
'request_name' => $requestName,
'attempts' => $attempt,
]);
throw $exception;
}
$delay = $retryStrategy->getDelay($attempt);
$this->logger?->info("[ApiGateway] Retrying after delay", [
'operation_id' => $operationId,
'service' => $service,
'request_name' => $requestName,
'attempt' => $attempt,
'delay_ms' => $delay->toMilliseconds(),
]);
// Sleep before retry
usleep($delay->toMicroseconds());
}
}
}
/**
* Record metrics from performance snapshot
*/
private function recordMetrics(
string $service,
string $requestName,
\App\Framework\Performance\ValueObjects\PerformanceSnapshot $snapshot,
bool $success,
int $retryAttempts,
bool $circuitBreakerTriggered
): void {
$this->metrics->recordRequest(
service: $service,
requestName: $requestName,
durationMs: $snapshot->duration->toMilliseconds(),
success: $success,
retryAttempts: $retryAttempts,
circuitBreakerTriggered: $circuitBreakerTriggered
);
}
}