diff --git a/.env b/.env index 9f6050fe50..86d7c558c4 100644 --- a/.env +++ b/.env @@ -43,3 +43,5 @@ _APP_MAINTENANCE_RETENTION_EXECUTION=1209600 _APP_MAINTENANCE_RETENTION_ABUSE=86400 _APP_MAINTENANCE_RETENTION_AUDIT=1209600 _APP_USAGE_STATS=enabled +_APP_LOGGING_PROVIDER= +_APP_LOGGING_CONFIG= diff --git a/.travis.yml_tmp b/.travis.yml_tmp index e433217a96..197f30923a 100644 --- a/.travis.yml_tmp +++ b/.travis.yml_tmp @@ -65,6 +65,7 @@ script: exit 1 fi - docker-compose logs appwrite +- docker-compose logs appwrite-realtime - docker-compose logs mariadb - docker-compose logs appwrite-worker-functions - docker-compose exec appwrite doctor diff --git a/Dockerfile b/Dockerfile index 8af07f2e93..fe515f7b53 100755 --- a/Dockerfile +++ b/Dockerfile @@ -181,7 +181,9 @@ ENV _APP_SERVER=swoole \ _APP_MAINTENANCE_RETENTION_AUDIT=1209600 \ # 1 Day = 86400 s _APP_MAINTENANCE_RETENTION_ABUSE=86400 \ - _APP_MAINTENANCE_INTERVAL=86400 + _APP_MAINTENANCE_INTERVAL=86400 \ + _APP_LOGGING_PROVIDER= \ + _APP_LOGGING_CONFIG= RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone diff --git a/app/config/variables.php b/app/config/variables.php index 5e5e813065..43687110ae 100644 --- a/app/config/variables.php +++ b/app/config/variables.php @@ -150,6 +150,24 @@ return [ 'question' => '', 'filter' => '' ], + [ + 'name' => '_APP_LOGGING_PROVIDER', + 'description' => 'This variable allows you to enable logging errors to 3rd party providers. This value is empty by default, to enable the logger set the value to one of \'sentry\', \'raygun\', \'appsignal\'', + 'introduction' => '0.12.0', + 'default' => '', + 'required' => false, + 'question' => '', + 'filter' => '' + ], + [ + 'name' => '_APP_LOGGING_CONFIG', + 'description' => 'This variable configures authentication to 3rd party error logging providers. If using Sentry, this should be \'SENTRY_API_KEY;SENTRY_APP_ID\'. If using Raygun, this should be Raygun API key. If using AppSignal, this should be AppSignal API key.', + 'introduction' => '0.12.0', + 'default' => '', + 'required' => false, + 'question' => '', + 'filter' => '' + ], [ 'name' => '_APP_USAGE_AGGREGATION_INTERVAL', 'description' => 'Interval value containing the number of seconds that the Appwrite usage process should wait before aggregating stats and syncing it to mariadb from InfluxDB. The default value is 30 seconds.', diff --git a/app/controllers/general.php b/app/controllers/general.php index deb36a1b45..e82ed60b12 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -3,6 +3,8 @@ require_once __DIR__.'/../init.php'; use Utopia\App; +use Utopia\Logger\Log; +use Utopia\Logger\Log\User; use Utopia\Swoole\Request; use Appwrite\Utopia\Response; use Appwrite\Utopia\View; @@ -298,19 +300,72 @@ App::options(function ($request, $response) { ->noContent(); }, ['request', 'response']); -App::error(function ($error, $utopia, $request, $response, $layout, $project) { +App::error(function ($error, $utopia, $request, $response, $layout, $project, $logger, $loggerBreadcrumbs) { /** @var Exception $error */ /** @var Utopia\App $utopia */ /** @var Utopia\Swoole\Request $request */ /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Utopia\View $layout */ /** @var Utopia\Database\Document $project */ + /** @var Utopia\Logger\Logger $logger */ + /** @var Utopia\Logger\Log\Breadcrumb[] $loggerBreadcrumbs */ + + $version = App::getEnv('_APP_VERSION', 'UNKNOWN'); + $route = $utopia->match($request); + + if($logger) { + if($error->getCode() >= 500 || $error->getCode() === 0) { + try { + $user = $utopia->getResource('user'); + /** @var Appwrite\Database\Document $user */ + } catch(\Throwable $th) { + // All good, user is optional information for logger + } + + $log = new Utopia\Logger\Log(); + + if(isset($user) && !$user->isEmpty()) { + $log->setUser(new User($user->getId())); + } + + $log->setNamespace("http"); + $log->setServer(\gethostname()); + $log->setVersion($version); + $log->setType(Log::TYPE_ERROR); + $log->setMessage($error->getMessage()); + + $log->addTag('method', $route->getMethod()); + $log->addTag('url', $route->getPath()); + $log->addTag('verboseType', get_class($error)); + $log->addTag('code', $error->getCode()); + $log->addTag('projectId', $project->getId()); + $log->addTag('hostname', $request->getHostname()); + $log->addTag('locale', (string)$request->getParam('locale', $request->getHeader('x-appwrite-locale', ''))); + + $log->addExtra('file', $error->getFile()); + $log->addExtra('line', $error->getLine()); + $log->addExtra('trace', $error->getTraceAsString()); + $log->addExtra('roles', Authorization::$roles); + + $action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD"); + $log->setAction($action); + + $isProduction = App::getEnv('_APP_ENV', 'development') === 'production'; + $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING); + + foreach($loggerBreadcrumbs as $loggerBreadcrumb) { + $log->addBreadcrumb($loggerBreadcrumb); + } + + $responseCode = $logger->addLog($log); + Console::info('Log pushed with status code: '.$responseCode); + } + } if ($error instanceof PDOException) { throw $error; } - $route = $utopia->match($request); $template = ($route) ? $route->getLabel('error', null) : null; if (php_sapi_name() === 'cli') { @@ -327,8 +382,6 @@ App::error(function ($error, $utopia, $request, $response, $layout, $project) { Console::error('[Error] Line: '.$error->getLine()); } - $version = App::getEnv('_APP_VERSION', 'UNKNOWN'); - switch ($error->getCode()) { // Don't show 500 errors! case 400: // Error allowed publicly case 401: // Error allowed publicly @@ -395,7 +448,7 @@ App::error(function ($error, $utopia, $request, $response, $layout, $project) { $response->dynamic(new Document($output), $utopia->isDevelopment() ? Response::MODEL_ERROR_DEV : Response::MODEL_ERROR); -}, ['error', 'utopia', 'request', 'response', 'layout', 'project']); +}, ['error', 'utopia', 'request', 'response', 'layout', 'project', 'logger', 'loggerBreadcrumbs']); App::get('/manifest.json') ->desc('Progressive app manifest file') diff --git a/app/http.php b/app/http.php index 13223edfbc..d37bcbf9a5 100644 --- a/app/http.php +++ b/app/http.php @@ -16,6 +16,8 @@ use Utopia\Abuse\Adapters\TimeLimit; use Utopia\Database\Document; use Utopia\Swoole\Files; use Utopia\Swoole\Request; +use Utopia\Logger\Log; +use Utopia\Logger\Log\User; $http = new Server("0.0.0.0", App::getEnv('PORT', 80)); @@ -186,6 +188,59 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo $app->run($request, $response); } catch (\Throwable $th) { + $version = App::getEnv('_APP_VERSION', 'UNKNOWN'); + + $logger = $app->getResource("logger"); + if($logger) { + try { + $user = $app->getResource('user'); + /** @var Appwrite\Database\Document $user */ + } catch(\Throwable $_th) { + // All good, user is optional information for logger + } + + $loggerBreadcrumbs = $app->getResource("loggerBreadcrumbs"); + $route = $app->match($request); + + $log = new Utopia\Logger\Log(); + + if(isset($user) && !$user->isEmpty()) { + $log->setUser(new User($user->getId())); + } + + $log->setNamespace("http"); + $log->setServer(\gethostname()); + $log->setVersion($version); + $log->setType(Log::TYPE_ERROR); + $log->setMessage($th->getMessage()); + + $log->addTag('method', $route->getMethod()); + $log->addTag('url', $route->getPath()); + $log->addTag('verboseType', get_class($th)); + $log->addTag('code', $th->getCode()); + // $log->addTag('projectId', $project->getId()); // TODO: Figure out how to get ProjectID, if it becomes relevant + $log->addTag('hostname', $request->getHostname()); + $log->addTag('locale', (string)$request->getParam('locale', $request->getHeader('x-appwrite-locale', ''))); + + $log->addExtra('file', $th->getFile()); + $log->addExtra('line', $th->getLine()); + $log->addExtra('trace', $th->getTraceAsString()); + $log->addExtra('roles', Authorization::$roles); + + $action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD"); + $log->setAction($action); + + $isProduction = App::getEnv('_APP_ENV', 'development') === 'production'; + $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING); + + foreach($loggerBreadcrumbs as $loggerBreadcrumb) { + $log->addBreadcrumb($loggerBreadcrumb); + } + + $responseCode = $logger->addLog($log); + Console::info('Log pushed with status code: '.$responseCode); + } + Console::error('[Error] Type: '.get_class($th)); Console::error('[Error] Message: '.$th->getMessage()); Console::error('[Error] File: '.$th->getFile()); @@ -200,12 +255,20 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo $swooleResponse->setStatusCode(500); - if(App::isDevelopment()) { - $swooleResponse->end('error: '.$th->getMessage()); - } - else { - $swooleResponse->end('500: Server Error'); - } + $output = ((App::isDevelopment())) ? [ + 'message' => 'Error: '. $th->getMessage(), + 'code' => 500, + 'file' => $th->getFile(), + 'line' => $th->getLine(), + 'trace' => $th->getTrace(), + 'version' => $version, + ] : [ + 'message' => 'Error: Server Error', + 'code' => 500, + 'version' => $version, + ]; + + $swooleResponse->end(\json_encode($output)); } finally { /** @var PDOPool $dbPool */ $dbPool = $register->get('dbPool'); diff --git a/app/init.php b/app/init.php index 3f74f34c01..89f2361d2b 100644 --- a/app/init.php +++ b/app/init.php @@ -30,6 +30,7 @@ use Appwrite\OpenSSL\OpenSSL; use Appwrite\Stats\Stats; use Appwrite\Utopia\View; use Utopia\App; +use Utopia\Logger\Logger; use Utopia\Config\Config; use Utopia\Locale\Locale; use Utopia\Registry\Registry; @@ -144,7 +145,7 @@ Config::load('locale-continents', __DIR__.'/config/locale/continents.php'); Config::load('storage-logos', __DIR__.'/config/storage/logos.php'); Config::load('storage-mimes', __DIR__.'/config/storage/mimes.php'); Config::load('storage-inputs', __DIR__.'/config/storage/inputs.php'); -Config::load('storage-outputs', __DIR__.'/config/storage/outputs.php'); +Config::load('storage-outputs', __DIR__.'/config/storage/outputs.php'); $user = App::getEnv('_APP_REDIS_USER',''); $pass = App::getEnv('_APP_REDIS_PASS',''); @@ -375,6 +376,22 @@ Structure::addFormat(APP_DATABASE_ATTRIBUTE_FLOAT_RANGE, function($attribute) { /* * Registry */ +$register->set('logger', function () { // Register error logger + $providerName = App::getEnv('_APP_LOGGING_PROVIDER', ''); + $providerConfig = App::getEnv('_APP_LOGGING_CONFIG', ''); + + if(empty($providerName) || empty($providerConfig)) { + return null; + } + + if(!Logger::hasProvider($providerName)) { + throw new Exception("Logging provider not supported. Logging disabled."); + } + + $classname = '\\Utopia\\Logger\\Adapter\\'.\ucfirst($providerName); + $adapter = new $classname($providerConfig); + return new Logger($adapter); +}); $register->set('dbPool', function () { // Register DB connection $dbHost = App::getEnv('_APP_DB_HOST', ''); $dbPort = App::getEnv('_APP_DB_PORT', ''); @@ -581,6 +598,14 @@ Locale::setLanguageFromJSON('zh-tw', __DIR__.'/config/locale/translations/zh-tw. ]); // Runtime Execution +App::setResource('logger', function($register) { + return $register->get('logger'); +}, ['register']); + +App::setResource('loggerBreadcrumbs', function() { + return []; +}); + App::setResource('register', fn() => $register); App::setResource('layout', function($locale) { diff --git a/app/realtime.php b/app/realtime.php index a671e149a3..aff6ec227b 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -14,6 +14,8 @@ use Utopia\Abuse\Abuse; use Utopia\Abuse\Adapters\TimeLimit; use Utopia\App; use Utopia\CLI\Console; +use Utopia\Config\Config; +use Utopia\Logger\Log; use Utopia\Database\Database; use Utopia\Cache\Adapter\Redis as RedisCache; use Utopia\Cache\Cache; @@ -51,6 +53,43 @@ $adapter->setPackageMaxLength(64000); // Default maximum Package Size (64kb) $server = new Server($adapter); +$logError = function(Throwable $error, string $action) use ($register) { + $logger = $register->get('logger'); + + if($logger) { + $version = App::getEnv('_APP_VERSION', 'UNKNOWN'); + + $log = new Log(); + $log->setNamespace("realtime"); + $log->setServer(\gethostname()); + $log->setVersion($version); + $log->setType(Log::TYPE_ERROR); + $log->setMessage($error->getMessage()); + + $log->addTag('code', $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); + + $isProduction = App::getEnv('_APP_ENV', 'development') === 'production'; + $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING); + + $responseCode = $logger->addLog($log); + Console::info('Realtime log pushed with status code: '.$responseCode); + } + + 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()); +}; + +$server->error($logError); + function getDatabase(Registry &$register, string $namespace) { $db = $register->get('dbPool')->get(); @@ -70,13 +109,13 @@ function getDatabase(Registry &$register, string $namespace) ]; }; -$server->onStart(function () use ($stats, $register, $containerId, &$statsDocument) { +$server->onStart(function () use ($stats, $register, $containerId, &$statsDocument, $logError) { Console::success('Server started succefully'); /** * Create document for this worker to share stats across Containers. */ - go(function () use ($register, $containerId, &$statsDocument) { + go(function () use ($register, $containerId, &$statsDocument, $logError) { try { [$database, $returnDatabase] = getDatabase($register, '_project_console'); $document = new Document([ @@ -90,10 +129,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume ]); $statsDocument = Authorization::skip(fn() => $database->createDocument('realtime', $document)); } catch (\Throwable $th) { - Console::error('[Error] Type: ' . get_class($th)); - Console::error('[Error] Message: ' . $th->getMessage()); - Console::error('[Error] File: ' . $th->getFile()); - Console::error('[Error] Line: ' . $th->getLine()); + call_user_func($logError, $th, "createWorkerDocument"); } finally { call_user_func($returnDatabase); } @@ -102,7 +138,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume /** * Save current connections to the Database every 5 seconds. */ - Timer::tick(5000, function () use ($register, $stats, $containerId, &$statsDocument) { + Timer::tick(5000, function () use ($register, $stats, $containerId, &$statsDocument, $logError) { /** @var Document $statsDocument */ foreach ($stats as $projectId => $value) { $connections = $stats->get($projectId, 'connections') ?? 0; @@ -142,23 +178,20 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume Authorization::skip(fn() => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument)); } catch (\Throwable $th) { - Console::error('[Error] Type: ' . get_class($th)); - Console::error('[Error] Message: ' . $th->getMessage()); - Console::error('[Error] File: ' . $th->getFile()); - Console::error('[Error] Line: ' . $th->getLine()); + call_user_func($logError, $th, "updateWorkerDocument"); } finally { call_user_func($returnDatabase); } }); }); -$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime) { +$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime, $logError) { Console::success('Worker ' . $workerId . ' started succefully'); $attempts = 0; $start = time(); - Timer::tick(5000, function () use ($server, $register, $realtime, $stats) { + Timer::tick(5000, function () use ($server, $register, $realtime, $stats, $logError) { /** * Sending current connections to project channels on the console project every 5 seconds. */ @@ -300,6 +333,8 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, } }); } catch (\Throwable $th) { + call_user_func($logError, $th, "pubSubConnection"); + Console::error('Pub/sub error: ' . $th->getMessage()); $register->get('redisPool')->put($redis); $attempts++; @@ -312,7 +347,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, Console::error('Failed to restart pub/sub...'); }); -$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime) { +$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime, $logError) { $app = new App('UTC'); $request = new Request($request); $response = new Response(new SwooleResponse()); @@ -409,6 +444,8 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $stats->incr($project->getId(), 'connections'); $stats->incr($project->getId(), 'connectionsTotal'); } catch (\Throwable $th) { + call_user_func($logError, $th, "initServer"); + $response = [ 'type' => 'error', 'data' => [ diff --git a/app/tasks/doctor.php b/app/tasks/doctor.php index 47e523ff94..20cafdf744 100644 --- a/app/tasks/doctor.php +++ b/app/tasks/doctor.php @@ -3,6 +3,7 @@ global $cli; use Appwrite\ClamAV\Network; +use Utopia\Logger\Logger; use Utopia\Storage\Device\Local; use Utopia\Storage\Storage; use Utopia\App; @@ -82,6 +83,16 @@ $cli Console::log('🟢 HTTPS force option is enabled'); } + + $providerName = App::getEnv('_APP_LOGGING_PROVIDER', ''); + $providerConfig = App::getEnv('_APP_LOGGING_CONFIG', ''); + + if(empty($providerName) || empty($providerConfig) || !Logger::hasProvider($providerName)) { + Console::log('🔴 Logging adapter is disabled'); + } else { + Console::log('🟢 Logging adapter is enabled (' . $providerName . ')'); + } + \sleep(0.2); try { diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml index 0756d29c07..5172b11119 100644 --- a/app/views/install/compose.phtml +++ b/app/views/install/compose.phtml @@ -106,6 +106,8 @@ services: - _APP_FUNCTIONS_MEMORY - _APP_FUNCTIONS_MEMORY_SWAP - _APP_FUNCTIONS_RUNTIMES + - _APP_LOGGING_PROVIDER + - _APP_LOGGING_CONFIG - _APP_STATSD_HOST - _APP_STATSD_PORT - _APP_MAINTENANCE_INTERVAL diff --git a/app/workers/audits.php b/app/workers/audits.php index 10952d483b..d83c832c37 100644 --- a/app/workers/audits.php +++ b/app/workers/audits.php @@ -11,6 +11,10 @@ Console::success(APP_NAME . ' audits worker v1 has started'); class AuditsV1 extends Worker { + public function getName(): string { + return "audits"; + } + public function init(): void { } diff --git a/app/workers/certificates.php b/app/workers/certificates.php index b7f899825a..9f9ce33dbd 100644 --- a/app/workers/certificates.php +++ b/app/workers/certificates.php @@ -16,6 +16,10 @@ Console::success(APP_NAME . ' certificates worker v1 has started'); class CertificatesV1 extends Worker { + public function getName(): string { + return "certificates"; + } + public function init(): void { } diff --git a/app/workers/deletes.php b/app/workers/deletes.php index 322d60305c..c1cd23bf48 100644 --- a/app/workers/deletes.php +++ b/app/workers/deletes.php @@ -23,6 +23,10 @@ class DeletesV1 extends Worker */ protected $consoleDB = null; + public function getName(): string { + return "deletes"; + } + public function init(): void { } diff --git a/app/workers/functions.php b/app/workers/functions.php index 4444dc7b13..29a92d93d6 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -100,6 +100,10 @@ class FunctionsV1 extends Worker public array $allowed = []; + public function getName(): string { + return "functions"; + } + public function init(): void { } diff --git a/app/workers/mails.php b/app/workers/mails.php index 9ec2dd97c0..25ddb438c0 100644 --- a/app/workers/mails.php +++ b/app/workers/mails.php @@ -13,6 +13,10 @@ Console::success(APP_NAME . ' mails worker v1 has started' . "\n"); class MailsV1 extends Worker { + public function getName(): string { + return "mails"; + } + public function init(): void { } diff --git a/app/workers/webhooks.php b/app/workers/webhooks.php index 28db64a606..8613472216 100644 --- a/app/workers/webhooks.php +++ b/app/workers/webhooks.php @@ -11,6 +11,10 @@ Console::success(APP_NAME . ' webhooks worker v1 has started'); class WebhooksV1 extends Worker { + public function getName(): string { + return "webhooks"; + } + public function init(): void { } diff --git a/composer.json b/composer.json index 7fd4727215..83d7d4fdbd 100644 --- a/composer.json +++ b/composer.json @@ -39,6 +39,7 @@ "appwrite/php-runtimes": "0.6.*", "utopia-php/framework": "0.19.*", + "utopia-php/logger": "0.1.*", "utopia-php/abuse": "0.7.*", "utopia-php/analytics": "0.2.*", "utopia-php/audit": "0.8.*", @@ -53,7 +54,7 @@ "utopia-php/domains": "1.1.*", "utopia-php/swoole": "0.3.*", "utopia-php/storage": "0.5.*", - "utopia-php/websocket": "0.0.*", + "utopia-php/websocket": "0.1.0", "utopia-php/image": "0.5.*", "resque/php-resque": "1.3.6", diff --git a/composer.lock b/composer.lock index 590d207c91..7fb2cd49a5 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": "2b1ed15e618832ee86b69cff2dcdd6ac", + "content-hash": "e0243d2a276d074c4af4ac21f521c953", "packages": [ { "name": "adhocore/jwt", @@ -2405,6 +2405,72 @@ }, "time": "2021-07-24T11:35:55+00:00" }, + { + "name": "utopia-php/logger", + "version": "0.1.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/logger.git", + "reference": "a7d626e349e8736e46d4d75f5ba686b40e73c097" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/logger/zipball/a7d626e349e8736e46d4d75f5ba686b40e73c097", + "reference": "a7d626e349e8736e46d4d75f5ba686b40e73c097", + "shasum": "" + }, + "require": { + "php": ">=8.0" + }, + "require-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" + ], + "authors": [ + { + "name": "Eldad Fux", + "email": "eldad@appwrite.io" + }, + { + "name": "Matej Bačo", + "email": "matej@appwrite.io" + }, + { + "name": "Christy Jacob", + "email": "christy@appwrite.io" + } + ], + "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.1.0" + }, + "time": "2021-12-20T06:57:26+00:00" + }, { "name": "utopia-php/orchestration", "version": "0.2.1", @@ -2730,16 +2796,16 @@ }, { "name": "utopia-php/websocket", - "version": "0.0.1", + "version": "0.1.0", "source": { "type": "git", "url": "https://github.com/utopia-php/websocket.git", - "reference": "808317ef4ea0683c2c82dee5d543b1c8378e2e1b" + "reference": "51fcb86171400d8aa40d76c54593481fd273dab5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/websocket/zipball/808317ef4ea0683c2c82dee5d543b1c8378e2e1b", - "reference": "808317ef4ea0683c2c82dee5d543b1c8378e2e1b", + "url": "https://api.github.com/repos/utopia-php/websocket/zipball/51fcb86171400d8aa40d76c54593481fd273dab5", + "reference": "51fcb86171400d8aa40d76c54593481fd273dab5", "shasum": "" }, "require": { @@ -2782,9 +2848,9 @@ ], "support": { "issues": "https://github.com/utopia-php/websocket/issues", - "source": "https://github.com/utopia-php/websocket/tree/0.0.1" + "source": "https://github.com/utopia-php/websocket/tree/0.1.0" }, - "time": "2021-07-11T13:09:44+00:00" + "time": "2021-12-20T10:50:09+00:00" }, { "name": "webmozart/assert", @@ -6520,5 +6586,5 @@ "platform-overrides": { "php": "8.0" }, - "plugin-api-version": "2.0.0" + "plugin-api-version": "2.1.0" } diff --git a/docker-compose.yml b/docker-compose.yml index 2efed94a41..2e21820a66 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -125,6 +125,8 @@ services: - _APP_FUNCTIONS_MEMORY - _APP_FUNCTIONS_MEMORY_SWAP - _APP_FUNCTIONS_RUNTIMES + - _APP_LOGGING_PROVIDER + - _APP_LOGGING_CONFIG appwrite-realtime: entrypoint: realtime @@ -154,6 +156,7 @@ services: volumes: - ./app:/usr/src/code/app - ./src:/usr/src/code/src + # - ./vendor:/usr/src/code/vendor depends_on: - redis environment: @@ -168,6 +171,8 @@ services: - _APP_DB_USER - _APP_DB_PASS - _APP_USAGE_STATS + - _APP_LOGGING_PROVIDER + - _APP_LOGGING_CONFIG appwrite-worker-audits: entrypoint: worker-audits @@ -193,6 +198,8 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS + - _APP_LOGGING_PROVIDER + - _APP_LOGGING_CONFIG appwrite-worker-webhooks: entrypoint: worker-webhooks @@ -215,6 +222,8 @@ services: - _APP_REDIS_PORT - _APP_REDIS_USER - _APP_REDIS_PASS + - _APP_LOGGING_PROVIDER + - _APP_LOGGING_CONFIG appwrite-worker-deletes: entrypoint: worker-deletes @@ -244,6 +253,8 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS + - _APP_LOGGING_PROVIDER + - _APP_LOGGING_CONFIG appwrite-worker-database: entrypoint: worker-database @@ -270,6 +281,8 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS + - _APP_LOGGING_PROVIDER + - _APP_LOGGING_CONFIG appwrite-worker-certificates: entrypoint: worker-certificates @@ -299,6 +312,8 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS + - _APP_LOGGING_PROVIDER + - _APP_LOGGING_CONFIG appwrite-worker-functions: entrypoint: worker-functions @@ -339,6 +354,8 @@ services: - _APP_STATSD_PORT - DOCKERHUB_PULL_USERNAME - DOCKERHUB_PULL_PASSWORD + - _APP_LOGGING_PROVIDER + - _APP_LOGGING_CONFIG appwrite-worker-mails: entrypoint: worker-mails @@ -367,6 +384,8 @@ services: - _APP_SMTP_SECURE - _APP_SMTP_USERNAME - _APP_SMTP_PASSWORD + - _APP_LOGGING_PROVIDER + - _APP_LOGGING_CONFIG appwrite-maintenance: entrypoint: maintenance diff --git a/src/Appwrite/Resque/Worker.php b/src/Appwrite/Resque/Worker.php index a09d00ee5e..1444c90663 100644 --- a/src/Appwrite/Resque/Worker.php +++ b/src/Appwrite/Resque/Worker.php @@ -9,15 +9,66 @@ use Utopia\CLI\Console; use Utopia\Database\Database; use Utopia\Database\Adapter\MariaDB; +use Exception; abstract class Worker { + /** + * Callbacks that will be executed when an error occurs + * + * @var array + */ + static protected array $errorCallbacks = []; + + /** + * Associative array holding all information passed into the worker + * + * @return array + */ public array $args = []; - abstract public function init(): void; + /** + * Function for identifying the worker needs to be set to unique name + * + * @return string + * @throws Exception + */ + public function getName(): string + { + throw new Exception("Please implement getName method in worker"); + } - abstract public function run(): void; + /** + * Function executed before running first task. + * Can include any preparations, such as connecting to external services or loading files + * + * @return void + * @throws \Exception|\Throwable + */ + public function init() { + throw new Exception("Please implement getName method in worker"); + } - abstract public function shutdown(): void; + /** + * Function executed when new task requests is received. + * You can access $args here, it will contain event information + * + * @return void + * @throws \Exception|\Throwable + */ + public function run() { + throw new Exception("Please implement getName method in worker"); + } + + /** + * Function executed just before shutting down the worker. + * You can do cleanup here, such as disconnecting from services or removing temp files + * + * @return void + * @throws \Exception|\Throwable + */ + public function shutdown() { + throw new Exception("Please implement getName method in worker"); + } const MAX_ATTEMPTS = 10; const SLEEP_TIME = 2; @@ -25,19 +76,73 @@ abstract class Worker const DATABASE_PROJECT = 'project'; const DATABASE_CONSOLE = 'console'; + /** + * A wrapper around 'init' function with non-worker-specific code + * + * @return void + * @throws \Exception|\Throwable + */ public function setUp(): void { - $this->init(); + try { + $this->init(); + } catch(\Throwable $error) { + foreach (self::$errorCallbacks as $errorCallback) { + $errorCallback($error, "init", $this->getName()); + } + + throw $error; + } } + /** + * A wrapper around 'run' function with non-worker-specific code + * + * @return void + * @throws \Exception|\Throwable + */ public function perform(): void { - $this->run(); + try { + $this->run(); + } catch(\Throwable $error) { + foreach (self::$errorCallbacks as $errorCallback) { + $errorCallback($error, "run", $this->getName(), $this->args); + } + + throw $error; + } } + /** + * A wrapper around 'shutdown' function with non-worker-specific code + * + * @return void + * @throws \Exception|\Throwable + */ public function tearDown(): void { - $this->shutdown(); + try { + $this->shutdown(); + } catch(\Throwable $error) { + foreach (self::$errorCallbacks as $errorCallback) { + $errorCallback($error, "shutdown", $this->getName()); + } + + throw $error; + } + } + + + /** + * Register callback. Will be executed when error occurs. + * @param callable $callback + * @param Throwable $error + * @return self + */ + public static function error(callable $callback): void + { + \array_push(self::$errorCallbacks, $callback); } /** * Get internal project database