diff --git a/.env b/.env index 9f6050fe5..86d7c558c 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 e433217a9..197f30923 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 8af07f2e9..fe515f7b5 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 5e5e81306..43687110a 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 deb36a1b4..e82ed60b1 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 13223edfb..d37bcbf9a 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 3f74f34c0..89f2361d2 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 a671e149a..aff6ec227 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 47e523ff9..20cafdf74 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 0756d29c0..5172b1111 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 10952d483..d83c832c3 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 b7f899825..9f9ce33db 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 322d60305..c1cd23bf4 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 4444dc7b1..29a92d93d 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 9ec2dd97c..25ddb438c 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 28db64a60..861347221 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 7fd472721..83d7d4fdb 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 590d207c9..7fb2cd49a 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 2efed94a4..2e21820a6 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 a09d00ee5..1444c9066 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