diff --git a/.env b/.env index b5cb7a1..ab3a863 100644 --- a/.env +++ b/.env @@ -1,4 +1,3 @@ GEO_ENV=development GEO_SECRET=geo-secret-key -GEO_LOGGING_PROVIDER= GEO_LOGGING_CONFIG= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 587012d..3b3f6da 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ composer.phar # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file # composer.lock test.http +.phpunit.result.cache diff --git a/composer.json b/composer.json index 3774d9b..3da13fd 100644 --- a/composer.json +++ b/composer.json @@ -2,16 +2,16 @@ "name": "appwrite/docker-geo", "type": "project", "require": { - "php": ">=8.3.0", + "php": ">=8.4", "utopia-php/fetch": "0.5.*", "utopia-php/http": "^2.0@RC", "utopia-php/platform": "^1.0@RC", "utopia-php/di": "0.3.*", "utopia-php/system": "^0.10.1", - "utopia-php/logger": "^0.7.1", "utopia-php/dsn": "^0.2.1", "utopia-php/cli": "^0.24", - "utopia-php/console": "^0.1.1" + "utopia-php/console": "^0.1.1", + "utopia-php/span": "^4.2" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index dc04db8..2a0ad4d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f46abbcdacdcd7c9a220cf6cc2f2ecfb", + "content-hash": "f2b25f41315c95c4a2d04768ddb4383a", "packages": [ { "name": "brick/math", @@ -1921,6 +1921,63 @@ }, "time": "2026-06-20T09:45:06+00:00" }, + { + "name": "utopia-php/client", + "version": "0.2.3", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/client.git", + "reference": "377dc1f79441ac1584f25dba57904ee1cf0f626b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/client/zipball/377dc1f79441ac1584f25dba57904ee1cf0f626b", + "reference": "377dc1f79441ac1584f25dba57904ee1cf0f626b", + "shasum": "" + }, + "require": { + "php": ">=8.5", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "utopia-php/pools": "^1.0", + "utopia-php/psr7": "^0.2", + "utopia-php/span": "^1.1 || ^3.0 || ^4.0" + }, + "require-dev": { + "swoole/ide-helper": "^6.0" + }, + "suggest": { + "ext-curl": "Required to use the cURL HTTP client adapter.", + "ext-simplexml": "Required to decode XML responses with Response::xml().", + "ext-swoole": "Required to use the Swoole coroutine HTTP client adapter." + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A lightweight PSR-18 HTTP client with cURL and Swoole coroutine backends", + "keywords": [ + "client", + "curl", + "http", + "php", + "psr-18", + "swoole", + "utopia" + ], + "support": { + "issues": "https://github.com/utopia-php/client/issues", + "source": "https://github.com/utopia-php/client/tree/0.2.3" + }, + "time": "2026-07-13T15:29:09+00:00" + }, { "name": "utopia-php/compression", "version": "0.1.6", @@ -2251,60 +2308,6 @@ }, "time": "2026-06-20T09:45:06+00:00" }, - { - "name": "utopia-php/logger", - "version": "0.7.1", - "source": { - "type": "git", - "url": "https://github.com/utopia-php/logger.git", - "reference": "b19cf23c2d58cfbc31d1571531b61ca6a93fd852" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/utopia-php/logger/zipball/b19cf23c2d58cfbc31d1571531b61ca6a93fd852", - "reference": "b19cf23c2d58cfbc31d1571531b61ca6a93fd852", - "shasum": "" - }, - "require": { - "php": ">=8.0" - }, - "require-dev": { - "laravel/pint": "1.2.*", - "phpstan/phpstan": "1.9.x-dev", - "phpunit/phpunit": "^9.3", - "vimeo/psalm": "4.0.1" - }, - "type": "library", - "autoload": { - "psr-4": { - "Utopia\\Logger\\": "src/Logger" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Utopia Logger library is simple and lite library for logging information, such as errors or warnings. This library is aiming to be as simple and easy to learn and use.", - "keywords": [ - "appsignal", - "errors", - "framework", - "logger", - "logging", - "logs", - "php", - "raygun", - "sentry", - "upf", - "utopia", - "warnings" - ], - "support": { - "issues": "https://github.com/utopia-php/logger/issues", - "source": "https://github.com/utopia-php/logger/tree/0.7.1" - }, - "time": "2025-11-12T08:27:12+00:00" - }, { "name": "utopia-php/platform", "version": "1.0.0-rc11", @@ -2405,6 +2408,52 @@ }, "time": "2026-06-20T09:45:06+00:00" }, + { + "name": "utopia-php/psr7", + "version": "0.2.2", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/psr7.git", + "reference": "cf70bec9ae4cda33bba01c8f7f2194d32adf899d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/psr7/zipball/cf70bec9ae4cda33bba01c8f7f2194d32adf899d", + "reference": "cf70bec9ae4cda33bba01c8f7f2194d32adf899d", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-simplexml": "Required to decode XML responses with Response::xml()." + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Psr7\\": "src/Psr7/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PSR-7 HTTP message implementations and PSR-17 factories for Utopia", + "keywords": [ + "http", + "php", + "psr-17", + "psr-7", + "utopia" + ], + "support": { + "issues": "https://github.com/utopia-php/psr7/issues", + "source": "https://github.com/utopia-php/psr7/tree/0.2.2" + }, + "time": "2026-09-02T15:57:19+00:00" + }, { "name": "utopia-php/queue", "version": "0.21.2", @@ -2517,6 +2566,49 @@ }, "time": "2026-06-20T09:45:06+00:00" }, + { + "name": "utopia-php/span", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/span.git", + "reference": "8505a2331772bd84ca60333e01ce6b6d068ee1dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/span/zipball/8505a2331772bd84ca60333e01ce6b6d068ee1dc", + "reference": "8505a2331772bd84ca60333e01ce6b6d068ee1dc", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "psr/http-client": "^1.0", + "utopia-php/client": "^0.2 || ^0.3 || ^0.4", + "utopia-php/psr7": "^0.1 || ^0.2" + }, + "require-dev": { + "swoole/ide-helper": "^5.0" + }, + "suggest": { + "ext-swoole": "Required for coroutine-based storage" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Span\\": "src/Span/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Simple span tracing library for PHP with coroutine support", + "support": { + "issues": "https://github.com/utopia-php/span/issues", + "source": "https://github.com/utopia-php/span/tree/4.2.0" + }, + "time": "2026-08-28T00:40:32+00:00" + }, { "name": "utopia-php/system", "version": "0.10.1", @@ -4594,7 +4686,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=8.3.0" + "php": ">=8.4" }, "platform-dev": {}, "plugin-api-version": "2.9.0" diff --git a/docker-compose.yml b/docker-compose.yml index c139065..a897f20 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,7 +28,6 @@ services: - GEO_ENV - GEO_SECRET - GEO_DBIP_PATH - - GEO_LOGGING_PROVIDER - GEO_LOGGING_CONFIG tests: diff --git a/src/Geo/Modules/Core/Http/Error.php b/src/Geo/Modules/Core/Http/Error.php index bb65fe5..f0229e6 100644 --- a/src/Geo/Modules/Core/Http/Error.php +++ b/src/Geo/Modules/Core/Http/Error.php @@ -3,13 +3,11 @@ namespace Appwrite\Geo\Modules\Core\Http; use Throwable; -use Utopia\Console; use Utopia\Http\Http; use Utopia\Http\Response; use Utopia\Http\Route; -use Utopia\Logger\Log; -use Utopia\Logger\Logger; use Utopia\Platform\Action; +use Utopia\Span\Span; use Utopia\System\System; class Error extends Action @@ -27,14 +25,24 @@ public function __construct() ->groups(['*']) ->inject('route') ->inject('error') - ->inject('logger') ->inject('response') - ->callback(fn ($route, $error, $logger, $response) => $this->action($route, $error, $logger, $response)); + ->callback(fn ($route, $error, $response) => $this->action($route, $error, $response)); } - public function action(?Route $route, Throwable $error, ?Logger $logger, Response $response): void + public function action(?Route $route, Throwable $error, Response $response): void { - $this->logError(new Log(), $error, "httpError", $logger, $route); + $span = Span::current() ?? Span::init('http.request'); + $span->setError($error); + + // Client errors stay on the span but are not exported + $span->set('error.publish', $error->getCode() >= 500 || $error->getCode() === 0); + $span->set('error.type', \get_class($error)); + $span->set('error.code', $error->getCode()); + + if ($route) { + $span->set('http.method', $route->getMethod()); + $span->set('http.path', $route->getPath()); + } $version = System::getEnv('GEO_VERSION', 'UNKNOWN'); $message = $error->getMessage(); @@ -81,54 +89,7 @@ public function action(?Route $route, Throwable $error, ?Logger $logger, Respons ->setStatusCode($code); $response->json($output); - } - - protected function logError(Log $log, Throwable $error, string $action, ?Logger $logger = null, ?Route $route = null): void - { - $code = $error->getCode(); - $isServerError = $code >= 500 || $code === 0; - - if ($isServerError) { - Console::error('[Error] Type: ' . get_class($error)); - Console::error('[Error] Message: ' . $error->getMessage()); - Console::error('[Error] File: ' . $error->getFile()); - Console::error('[Error] Line: ' . $error->getLine()); - } - if ($logger && $isServerError) { - $version = (string) System::getEnv('GEO_VERSION', ''); - if (empty($version)) { - $version = 'UNKNOWN'; - } - - $log->setNamespace("geo"); - $log->setServer(\gethostname() !== false ? \gethostname() : null); - $log->setVersion($version); - $log->setType(Log::TYPE_ERROR); - $log->setMessage($error->getMessage()); - - if ($route) { - $log->addTag('method', $route->getMethod()); - $log->addTag('url', $route->getPath()); - } - - $log->addTag('code', \strval($error->getCode())); - $log->addTag('verboseType', get_class($error)); - - $log->addExtra('file', $error->getFile()); - $log->addExtra('line', $error->getLine()); - $log->addExtra('trace', $error->getTraceAsString()); - - $log->setAction($action); - - $log->setEnvironment(Http::isProduction() ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING); - try { - $responseCode = $logger->addLog($log); - Console::info('Geo log pushed with status code: ' . $responseCode); - } catch (Throwable $th) { - Console::error('Error pushing log: ' . $th->getMessage()); - Console::error($th->getTraceAsString()); - } - } + $span->finish(); } } diff --git a/src/Geo/Server/Server.php b/src/Geo/Server/Server.php index a299c34..0929305 100644 --- a/src/Geo/Server/Server.php +++ b/src/Geo/Server/Server.php @@ -4,19 +4,21 @@ use Appwrite\Geo\Platform\Geo; use Exception; +use InvalidArgumentException; +use MaxMind\Db\Reader; +use Throwable; use Utopia\Console; use Utopia\DI\Container; use Utopia\DSN\DSN; use Utopia\Http\Adapter\Swoole\Server as SwooleServer; use Utopia\Http\Http; -use MaxMind\Db\Reader; -use Throwable; -use Utopia\Logger\Adapter\AppSignal; -use Utopia\Logger\Adapter\LogOwl; -use Utopia\Logger\Adapter\Raygun; -use Utopia\Logger\Adapter\Sentry; -use Utopia\Logger\Logger; +use Utopia\Http\Request; use Utopia\Platform\Service; +use Utopia\Span\Exporter\Sentry; +use Utopia\Span\Exporter\SentryField; +use Utopia\Span\Exporter\Stdout; +use Utopia\Span\Span; +use Utopia\Span\Storage\Coroutine; use Utopia\System\System; class Server @@ -31,6 +33,7 @@ public function __construct(?Http $http = null) Http::setMode(System::getEnv('GEO_ENV', Http::MODE_TYPE_PRODUCTION)); + $this->initSpan(); $this->initResources(); $http ??= new Http( @@ -56,6 +59,17 @@ protected function initHooks(): void $onStart->action(function () { Console::log('Server started'); }); + + Http::onRequest() + ->inject('request') + ->action(function (Request $request) { + Span::init('http.request'); + Span::add('http.method', $request->getMethod()); + }); + + Http::shutdown() + ->groups(['*']) + ->action(fn () => Span::current()?->finish()); } protected function initResources(): void @@ -68,46 +82,42 @@ protected function initResources(): void } return new Reader($path); }); + } - $this->resources->set('logger', function () { - $providerName = System::getEnv('GEO_LOGGING_PROVIDER', ''); - $providerConfig = System::getEnv('GEO_LOGGING_CONFIG', ''); - - try { - $loggingProvider = new DSN($providerConfig); - - $providerName = $loggingProvider->getScheme(); - $providerConfig = match ($providerName) { - 'sentry' => ['key' => $loggingProvider->getPassword(), 'projectId' => $loggingProvider->getUser() ?? '', 'host' => 'https://' . $loggingProvider->getHost()], - 'logowl' => ['ticket' => $loggingProvider->getUser() ?? '', 'host' => $loggingProvider->getHost()], - default => ['key' => $loggingProvider->getHost()], - }; - } catch (Throwable) { - $configChunks = \explode(";", $providerConfig); - - $providerConfig = match ($providerName) { - 'sentry' => ['key' => $configChunks[0], 'projectId' => $configChunks[1] ?? '', 'host' => ''], - 'logowl' => ['ticket' => $configChunks[0], 'host' => ''], - default => ['key' => $providerConfig], - }; - } + protected function initSpan(): void + { + Span::setStorage(new Coroutine()); - $logger = null; + // Only server failures leave the process: the error action marks client errors error.publish=false + $sampler = static fn (Span $span): bool => $span->getError() !== null && $span->get('error.publish') !== false; - if (!empty($providerName) && Logger::hasProvider($providerName)) { - $adapter = match ($providerName) { - 'sentry' => new Sentry($providerConfig['projectId'] ?? '', $providerConfig['key'] ?? '', $providerConfig['host'] ?? ''), - 'logowl' => new LogOwl($providerConfig['ticket'] ?? '', $providerConfig['host'] ?? ''), - 'raygun' => new Raygun($providerConfig['key'] ?? ''), - 'appsignal' => new AppSignal($providerConfig['key'] ?? ''), - default => throw new Exception('Provider "' . $providerName . '" not supported.') - }; + $exporters = [new Stdout(sampler: $sampler)]; - $logger = new Logger($adapter); + // GEO_LOGGING_CONFIG: a sentry://PROJECT_ID:KEY@HOST DSN reports server errors to Sentry + $config = System::getEnv('GEO_LOGGING_CONFIG', ''); + if (!empty($config)) { + try { + $dsn = new DSN($config); + if ($dsn->getScheme() !== 'sentry') { + throw new InvalidArgumentException('Only the sentry:// scheme is supported'); + } + + $tags = ['http.method', 'http.path', 'error.type', 'error.code']; + $version = System::getEnv('GEO_VERSION', ''); + $exporters[] = new Sentry( + sampler: $sampler, + dsn: 'https://' . $dsn->getPassword() . '@' . $dsn->getHost() . '/' . $dsn->getUser(), + environment: Http::isProduction() ? 'production' : 'staging', + release: empty($version) ? 'UNKNOWN' : $version, + serverName: \gethostname() ?: null, + classifier: static fn (string $key): SentryField => \in_array($key, $tags, true) ? SentryField::Tag : SentryField::Context, + ); + } catch (Throwable $error) { + Console::error('Invalid GEO_LOGGING_CONFIG, error reporting is disabled: ' . $error->getMessage()); } + } - return $logger; - }); + Span::setExporters(...$exporters); } protected function initPlatform(): void diff --git a/tests/E2E/Base.php b/tests/E2E/Base.php index c3f94be..7423f00 100644 --- a/tests/E2E/Base.php +++ b/tests/E2E/Base.php @@ -41,13 +41,14 @@ protected function request(string $method, string $path, array $headers = []): a $body = \file_get_contents($url, false, $context); $statusCode = 0; $responseHeaders = []; + $headerLines = \http_get_last_response_headers(); - if (isset($http_response_header)) { + if (!empty($headerLines)) { // First line is status, e.g. "HTTP/1.1 200 OK" - if (\preg_match('/HTTP\/\S+\s+(\d+)/', $http_response_header[0], $matches)) { + if (\preg_match('/HTTP\/\S+\s+(\d+)/', $headerLines[0], $matches)) { $statusCode = (int) $matches[1]; } - foreach ($http_response_header as $header) { + foreach ($headerLines as $header) { if (\str_contains($header, ':')) { [$key, $value] = \explode(':', $header, 2); $responseHeaders[\strtolower(\trim($key))] = \trim($value);