Merge branch '1.9.x' of https://github.com/appwrite/appwrite into users-skip-targets

This commit is contained in:
fogelito
2026-05-14 14:58:50 +03:00
135 changed files with 3660 additions and 970 deletions
+10 -8
View File
@@ -2,10 +2,10 @@
require_once __DIR__ . '/init.php';
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Publisher\Certificate as CertificatePublisher;
use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Platform\Appwrite;
@@ -281,12 +281,14 @@ $container->set('publisherForStatsResources', fn (Publisher $publisher) => new S
$publisher,
new Queue(System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME))
), ['publisher']);
$container->set('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
$container->set('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
$container->set('publisherForFunctions', fn (Publisher $publisher) => new FunctionPublisher(
$publisher,
new Queue(System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME), 'utopia-queue', Event::FUNCTIONS_QUEUE_TTL)
), ['publisher']);
$container->set('publisherForDeletes', fn (Publisher $publisher) => new DeletePublisher(
$publisher,
new Queue(System::getEnv('_APP_DELETE_QUEUE_NAME', Event::DELETE_QUEUE_NAME))
), ['publisher']);
$container->set('logError', function (Registry $register) {
return function (Throwable $error, string $namespace, string $action) use ($register) {
Console::error('[Error] Timestamp: ' . date('c', time()));
+434
View File
@@ -1956,6 +1956,440 @@ $platformCollections = [
'attributes' => [],
'indexes' => []
],
'reports' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('reports'),
'name' => 'Reports',
'attributes' => [
[
'$id' => ID::custom('projectInternalId'),
'type' => Database::VAR_ID,
'format' => '',
'size' => 0,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('projectId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('appInternalId'),
'type' => Database::VAR_ID,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('appId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('type'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 64,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('title'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('summary'),
'type' => Database::VAR_TEXT,
'format' => '',
'size' => 65535,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => [],
],
[
// Resource type the report is about. Plural noun, e.g. databases, sites, urls.
'$id' => ID::custom('targetType'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 64,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
// Free-form target identifier (URL for lighthouse, resource ID for db).
// Indexed by `_key_project_target` with an explicit prefix length.
'$id' => ID::custom('target'),
'type' => Database::VAR_TEXT,
'format' => '',
'size' => 65535,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
// Category strings, e.g. 'performance', 'accessibility'. Native array
// column — we never query on individual entries (MySQL JSON-array
// indexes are weak), this is read+rewrite only.
'$id' => ID::custom('categories'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 64,
'signed' => true,
'required' => false,
'default' => null,
'array' => true,
'filters' => [],
],
[
// Virtual attribute — insights live in the `insights` collection
// back-referenced by `reportInternalId`. The subQuery filter joins
// them at read time.
'$id' => ID::custom('insights'),
'type' => Database::VAR_TEXT,
'format' => '',
'size' => 65535,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['subQueryReportInsights'],
],
[
'$id' => ID::custom('analyzedAt'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
],
'indexes' => [
[
'$id' => ID::custom('_key_project_app_type'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId', 'appInternalId', 'type'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_project_target'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId', 'appInternalId', 'targetType', 'target'],
'lengths' => [null, null, null, 700],
'orders' => [],
],
],
],
'insights' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('insights'),
'name' => 'Insights',
'attributes' => [
[
'$id' => ID::custom('projectInternalId'),
'type' => Database::VAR_ID,
'format' => '',
'size' => 0,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('projectId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('reportInternalId'),
'type' => Database::VAR_ID,
'format' => '',
'size' => 0,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('reportId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('type'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 64,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('severity'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('status'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16,
'signed' => true,
'required' => true,
'default' => 'active',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('resourceType'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 64,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('resourceId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('resourceInternalId'),
'type' => Database::VAR_ID,
'format' => '',
'size' => 0,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('parentResourceType'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 64,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('parentResourceId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('parentResourceInternalId'),
'type' => Database::VAR_ID,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('title'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('summary'),
'type' => Database::VAR_TEXT,
'format' => '',
'size' => 65535,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('ctas'),
'type' => Database::VAR_TEXT,
'format' => '',
'size' => 65535,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['json'],
],
[
'$id' => ID::custom('analyzedAt'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('dismissedAt'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('dismissedBy'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => ID::custom('_key_project_report'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId', 'reportInternalId'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_project_resource'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId', 'resourceType', 'resourceId'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_project_parent_resource'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId', 'parentResourceType', 'parentResourceId'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_project_type'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId', 'type'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_project_severity'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId', 'severity'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_project_status'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId', 'status'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_project_dismissedAt'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId', 'dismissedAt'],
'lengths' => [],
'orders' => [Database::ORDER_ASC, Database::ORDER_DESC],
],
],
],
];
// Organization API keys subquery
+24
View File
@@ -1453,4 +1453,28 @@ return [
'description' => 'The maximum number of mock phones for this project has been reached.',
'code' => 400,
],
/** Advisor */
Exception::INSIGHT_NOT_FOUND => [
'name' => Exception::INSIGHT_NOT_FOUND,
'description' => 'Insight with the requested ID could not be found.',
'code' => 404,
],
Exception::INSIGHT_ALREADY_EXISTS => [
'name' => Exception::INSIGHT_ALREADY_EXISTS,
'description' => 'Insight with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409,
],
/** Reports */
Exception::REPORT_NOT_FOUND => [
'name' => Exception::REPORT_NOT_FOUND,
'description' => 'Report with the requested ID could not be found.',
'code' => 404,
],
Exception::REPORT_ALREADY_EXISTS => [
'name' => Exception::REPORT_ALREADY_EXISTS,
'description' => 'Report with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409,
],
];
+29 -1
View File
@@ -426,5 +426,33 @@ return [
'update' => [
'$description' => 'This event triggers when a proxy rule is updated.',
]
]
],
'reports' => [
'$model' => Response::MODEL_REPORT,
'$resource' => true,
'$description' => 'This event triggers on any report event.',
'create' => [
'$description' => 'This event triggers when a report is created.',
],
'update' => [
'$description' => 'This event triggers when a report is updated.',
],
'delete' => [
'$description' => 'This event triggers when a report is deleted.',
],
'insights' => [
'$model' => Response::MODEL_INSIGHT,
'$resource' => true,
'$description' => 'This event triggers on any insight event.',
'create' => [
'$description' => 'This event triggers when an insight is created.',
],
'update' => [
'$description' => 'This event triggers when an insight is updated.',
],
'delete' => [
'$description' => 'This event triggers when an insight is deleted.',
],
],
],
];
+4
View File
@@ -103,6 +103,10 @@ $admins = [
'tokens.write',
'schedules.read',
'schedules.write',
'insights.read',
'insights.write',
'reports.read',
'reports.write',
];
return [
+18
View File
@@ -361,4 +361,22 @@ return [
'description' => 'Access to create, update, and delete resources under VCS service.',
'category' => 'Other',
],
// Advisor
'insights.read' => [
'description' => 'Access to read insights under Advisor service.',
'category' => 'Advisor',
],
'insights.write' => [
'description' => 'Reserved for Advisor insight ingestion outside CE.',
'category' => 'Advisor',
],
'reports.read' => [
'description' => 'Access to read reports under Advisor service.',
'category' => 'Advisor',
],
'reports.write' => [
'description' => 'Access to delete reports under Advisor service.',
'category' => 'Advisor',
],
];
+15 -1
View File
@@ -308,5 +308,19 @@ return [
'optional' => true,
'icon' => '/images/services/messaging.png',
'platforms' => ['client', 'server', 'console'],
]
],
'advisor' => [
'key' => 'advisor',
'name' => 'Advisor',
'subtitle' => 'The Advisor service surfaces actionable reports about your project resources, with CTA descriptors for one-click remediation in the console.',
'description' => '/docs/services/advisor.md',
'controller' => '', // Uses modules
'sdk' => true,
'docs' => true,
'docsUrl' => 'https://appwrite.io/docs/server/advisor',
'tests' => true,
'optional' => true,
'icon' => '/images/services/insights.png',
'platforms' => ['server', 'console'],
],
];
+30 -23
View File
@@ -11,10 +11,11 @@ use Appwrite\Auth\Validator\PersonalData;
use Appwrite\Auth\Validator\Phone;
use Appwrite\Bus\Events\SessionCreated;
use Appwrite\Detector\Detector;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Message\Delete as DeleteMessage;
use Appwrite\Event\Message\Mail as MailMessage;
use Appwrite\Event\Message\Messaging as MessagingMessage;
use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Event\Publisher\Mail as MailPublisher;
use Appwrite\Event\Publisher\Messaging as MessagingPublisher;
use Appwrite\Extend\Exception;
@@ -472,9 +473,9 @@ Http::delete('/v1/account')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('queueForDeletes')
->inject('publisherForDeletes')
->inject('authorization')
->action(function (Document $user, Document $project, Response $response, Database $dbForProject, Event $queueForEvents, Delete $queueForDeletes, Authorization $authorization) {
->action(function (Document $user, Document $project, Response $response, Database $dbForProject, Event $queueForEvents, DeletePublisher $publisherForDeletes, Authorization $authorization) {
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
@@ -498,9 +499,11 @@ Http::delete('/v1/account')
$dbForProject->deleteDocument('users', $user->getId());
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($user);
$publisherForDeletes->enqueue(new DeleteMessage(
project: $project,
type: DELETE_TYPE_DOCUMENT,
document: $user,
));
$queueForEvents
->setParam('userId', $user->getId())
@@ -582,12 +585,12 @@ Http::delete('/v1/account/sessions')
->inject('dbForProject')
->inject('locale')
->inject('queueForEvents')
->inject('queueForDeletes')
->inject('publisherForDeletes')
->inject('store')
->inject('proofForToken')
->inject('domainVerification')
->inject('cookieDomain')
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) {
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, DeletePublisher $publisherForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) {
$protocol = $request->getProtocol();
$sessions = $user->getAttribute('sessions', []);
@@ -617,10 +620,11 @@ Http::delete('/v1/account/sessions')
$queueForEvents
->setPayload($response->output($session, Response::MODEL_SESSION));
$queueForDeletes
->setType(DELETE_TYPE_SESSION_TARGETS)
->setDocument($session)
->trigger();
$publisherForDeletes->enqueue(new DeleteMessage(
project: $queueForEvents->getProject(),
type: DELETE_TYPE_SESSION_TARGETS,
document: $session,
));
}
}
@@ -714,12 +718,12 @@ Http::delete('/v1/account/sessions/:sessionId')
->inject('dbForProject')
->inject('locale')
->inject('queueForEvents')
->inject('queueForDeletes')
->inject('publisherForDeletes')
->inject('store')
->inject('proofForToken')
->inject('domainVerification')
->inject('cookieDomain')
->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) {
->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, DeletePublisher $publisherForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) {
$protocol = $request->getProtocol();
$sessionId = ($sessionId === 'current')
@@ -761,10 +765,11 @@ Http::delete('/v1/account/sessions/:sessionId')
->setParam('sessionId', $session->getId())
->setPayload($response->output($session, Response::MODEL_SESSION));
$queueForDeletes
->setType(DELETE_TYPE_SESSION_TARGETS)
->setDocument($session)
->trigger();
$publisherForDeletes->enqueue(new DeleteMessage(
project: $queueForEvents->getProject(),
type: DELETE_TYPE_SESSION_TARGETS,
document: $session,
));
$response->noContent();
return;
@@ -4675,13 +4680,13 @@ Http::delete('/v1/account/targets/:targetId/push')
))
->param('targetId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Target ID.', false, ['dbForProject'])
->inject('queueForEvents')
->inject('queueForDeletes')
->inject('publisherForDeletes')
->inject('user')
->inject('request')
->inject('response')
->inject('dbForProject')
->inject('authorization')
->action(function (string $targetId, Event $queueForEvents, Delete $queueForDeletes, Document $user, Request $request, Response $response, Database $dbForProject, Authorization $authorization) {
->action(function (string $targetId, Event $queueForEvents, DeletePublisher $publisherForDeletes, Document $user, Request $request, Response $response, Database $dbForProject, Authorization $authorization) {
$target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $targetId));
if ($target->isEmpty()) {
@@ -4696,9 +4701,11 @@ Http::delete('/v1/account/targets/:targetId/push')
$dbForProject->purgeCachedDocument('users', $user->getId());
$queueForDeletes
->setType(DELETE_TYPE_TARGET)
->setDocument($target);
$publisherForDeletes->enqueue(new DeleteMessage(
project: $queueForEvents->getProject(),
type: DELETE_TYPE_TARGET,
document: $target,
));
$queueForEvents
->setParam('userId', $user->getId())
+9 -6
View File
@@ -3,9 +3,10 @@
use Ahc\Jwt\JWT;
use Appwrite\Auth\Validator\Phone;
use Appwrite\Detector\Detector;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Message\Delete as DeleteMessage;
use Appwrite\Event\Message\Messaging as MessagingMessage;
use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Event\Publisher\Messaging as MessagingPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Messaging\Status as MessageStatus;
@@ -2728,9 +2729,9 @@ Http::delete('/v1/messaging/topics/:topicId')
->param('topicId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Topic ID.', false, ['dbForProject'])
->inject('queueForEvents')
->inject('dbForProject')
->inject('queueForDeletes')
->inject('publisherForDeletes')
->inject('response')
->action(function (string $topicId, Event $queueForEvents, Database $dbForProject, Delete $queueForDeletes, Response $response) {
->action(function (string $topicId, Event $queueForEvents, Database $dbForProject, DeletePublisher $publisherForDeletes, Response $response) {
$topic = $dbForProject->getDocument('topics', $topicId);
if ($topic->isEmpty()) {
@@ -2739,9 +2740,11 @@ Http::delete('/v1/messaging/topics/:topicId')
$dbForProject->deleteDocument('topics', $topicId);
$queueForDeletes
->setType(DELETE_TYPE_TOPIC)
->setDocument($topic);
$publisherForDeletes->enqueue(new DeleteMessage(
project: $queueForEvents->getProject(),
type: DELETE_TYPE_TOPIC,
document: $topic,
));
$queueForEvents
->setParam('topicId', $topic->getId());
+16 -11
View File
@@ -11,8 +11,9 @@ use Appwrite\Auth\Validator\Phone;
use Appwrite\Deletes\Identities as DeleteIdentities;
use Appwrite\Deletes\Targets as DeleteTargets;
use Appwrite\Detector\Detector;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Message\Delete as DeleteMessage;
use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Hooks\Hooks;
use Appwrite\SDK\AuthType;
@@ -2624,8 +2625,8 @@ Http::delete('/v1/users/:userId')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('queueForDeletes')
->action(function (string $userId, Response $response, Database $dbForProject, Event $queueForEvents, Delete $queueForDeletes) {
->inject('publisherForDeletes')
->action(function (string $userId, Response $response, Database $dbForProject, Event $queueForEvents, DeletePublisher $publisherForDeletes) {
$user = $dbForProject->getDocument('users', $userId);
@@ -2640,9 +2641,11 @@ Http::delete('/v1/users/:userId')
DeleteIdentities::delete($dbForProject, Query::equal('userInternalId', [$user->getSequence()]));
DeleteTargets::delete($dbForProject, Query::equal('userInternalId', [$user->getSequence()]));
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($clone);
$publisherForDeletes->enqueue(new DeleteMessage(
project: $queueForEvents->getProject(),
type: DELETE_TYPE_DOCUMENT,
document: $clone,
));
$queueForEvents
->setParam('userId', $user->getId())
@@ -2675,10 +2678,10 @@ Http::delete('/v1/users/:userId/targets/:targetId')
->param('userId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'User ID.', false, ['dbForProject'])
->param('targetId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Target ID.', false, ['dbForProject'])
->inject('queueForEvents')
->inject('queueForDeletes')
->inject('publisherForDeletes')
->inject('response')
->inject('dbForProject')
->action(function (string $userId, string $targetId, Event $queueForEvents, Delete $queueForDeletes, Response $response, Database $dbForProject) {
->action(function (string $userId, string $targetId, Event $queueForEvents, DeletePublisher $publisherForDeletes, Response $response, Database $dbForProject) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
@@ -2698,9 +2701,11 @@ Http::delete('/v1/users/:userId/targets/:targetId')
$dbForProject->deleteDocument('targets', $target->getId());
$dbForProject->purgeCachedDocument('users', $user->getId());
$queueForDeletes
->setType(DELETE_TYPE_TARGET)
->setDocument($target);
$publisherForDeletes->enqueue(new DeleteMessage(
project: $queueForEvents->getProject(),
type: DELETE_TYPE_TARGET,
document: $target,
));
$queueForEvents
->setParam('userId', $user->getId())
+24 -23
View File
@@ -7,9 +7,10 @@ use Ahc\Jwt\JWTException;
use Appwrite\Auth\Key;
use Appwrite\Bus\Events\ExecutionCompleted;
use Appwrite\Bus\Events\RequestCompleted;
use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
use Appwrite\Event\Message\Delete as DeleteMessage;
use Appwrite\Event\Publisher\Certificate;
use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Network\Cors;
use Appwrite\Platform\Appwrite;
@@ -74,7 +75,7 @@ use Utopia\Validator\Text;
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeletePublisher $publisherForDeletes, int $executionsRetentionCount)
{
$host = $request->getHostname();
if (!empty($previewHostname)) {
@@ -790,12 +791,12 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
? RESOURCE_TYPE_FUNCTIONS
: RESOURCE_TYPE_SITES;
$queueForDeletes
->setProject($project)
->setResourceType($resourceType)
->setResource($resource->getSequence())
->setType(DELETE_TYPE_EXECUTIONS_LIMIT)
->trigger();
$publisherForDeletes->enqueue(new DeleteMessage(
project: $project,
type: DELETE_TYPE_EXECUTIONS_LIMIT,
resource: (string) $resource->getSequence(),
resourceType: $resourceType,
));
}
return true;
@@ -856,9 +857,9 @@ Http::init()
->inject('apiKey')
->inject('cors')
->inject('authorization')
->inject('queueForDeletes')
->inject('publisherForDeletes')
->inject('executionsRetentionCount')
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, Event $queueForEvents, Bus $bus, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, Event $queueForEvents, Bus $bus, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeletePublisher $publisherForDeletes, int $executionsRetentionCount) {
/*
* Appwrite Router
*/
@@ -866,7 +867,7 @@ Http::init()
$platformHostnames = $platform['hostnames'] ?? [];
// Only run Router when external domain
if (!\in_array($hostname, $platformHostnames) || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $publisherForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1167,16 +1168,16 @@ Http::options()
->inject('apiKey')
->inject('cors')
->inject('authorization')
->inject('queueForDeletes')
->inject('publisherForDeletes')
->inject('executionsRetentionCount')
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeletePublisher $publisherForDeletes, int $executionsRetentionCount) {
/*
* Appwrite Router
*/
$platformHostnames = $platform['hostnames'] ?? [];
// Only run Router when external domain
if (!in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $publisherForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1282,7 +1283,7 @@ Http::error()
if (!$publish && $project->getId() !== 'console') {
$errorUser = new DBUser();
try {
$resolvedUser = $utopia->getResource('user');
$resolvedUser = $utopia->context()->get('user');
if ($resolvedUser instanceof DBUser) {
$errorUser = $resolvedUser;
}
@@ -1301,7 +1302,7 @@ Http::error()
if ($logger && $publish) {
try {
/** @var Utopia\Database\Document $user */
$user = $utopia->getResource('user');
$user = $utopia->context()->get('user');
} catch (\Throwable) {
// All good, user is optional information for logger
}
@@ -1502,7 +1503,7 @@ Http::error()
// the cors resource (which depends on rule -> DB) would cascade.
// Uses override:true to avoid duplicate headers if init() already set them.
try {
$cors = $utopia->getResource('cors');
$cors = $utopia->context()->get('cors');
foreach ($cors->headers($request->getOrigin()) as $name => $value) {
$response
->removeHeader($name)
@@ -1569,15 +1570,15 @@ Http::get('/robots.txt')
->inject('previewHostname')
->inject('apiKey')
->inject('authorization')
->inject('queueForDeletes')
->inject('publisherForDeletes')
->inject('executionsRetentionCount')
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeletePublisher $publisherForDeletes, int $executionsRetentionCount) {
$platformHostnames = $platform['hostnames'] ?? [];
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/robots.phtml');
$response->text($template->render(false));
} else {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $publisherForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1603,15 +1604,15 @@ Http::get('/humans.txt')
->inject('previewHostname')
->inject('apiKey')
->inject('authorization')
->inject('queueForDeletes')
->inject('publisherForDeletes')
->inject('executionsRetentionCount')
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeletePublisher $publisherForDeletes, int $executionsRetentionCount) {
$platformHostnames = $platform['hostnames'] ?? [];
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/humans.phtml');
$response->text($template->render(false));
} else {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $publisherForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
+98 -88
View File
@@ -5,12 +5,12 @@ use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Bus\Events\RequestCompleted;
use Appwrite\Event\Context\Audit as AuditContext;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Message\Audit as AuditMessage;
use Appwrite\Event\Message\Func as FunctionMessage;
use Appwrite\Event\Message\Usage as UsageMessage;
use Appwrite\Event\Publisher\Audit;
use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
@@ -476,6 +476,85 @@ Http::init()
}
});
Http::init()
->groups(['api'])
->inject('utopia')
->inject('request')
->inject('response')
->inject('project')
->inject('user')
->inject('timelimit')
->inject('devKey')
->inject('authorization')
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, callable $timelimit, Document $devKey, Authorization $authorization) {
$response->setUser($user);
$request->setUser($user);
$roles = $authorization->getRoles();
$shouldCheckAbuse = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled'
&& ! $user->isApp($roles)
&& ! $user->isPrivileged($roles)
&& $devKey->isEmpty();
$route = $utopia->getRoute();
if ($route === null) {
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
}
$abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}');
$abuseKeyLabel = (! is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel;
$closestLimit = null;
foreach ($abuseKeyLabel as $abuseKey) {
$isRateLimited = false;
try {
$start = $request->getContentRangeStart();
$end = $request->getContentRangeEnd();
$timeLimit = $timelimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600));
$timeLimit
->setParam('{projectId}', $project->getId())
->setParam('{userId}', $user->getId())
->setParam('{userAgent}', $request->getUserAgent(''))
->setParam('{ip}', $request->getIP())
->setParam('{url}', $request->getHostname() . $route->getPath())
->setParam('{method}', $request->getMethod())
->setParam('{chunkId}', (int) ($start / ($end + 1 - $start)));
foreach ($request->getParams() as $key => $value) {
if (! empty($value)) {
$timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value);
}
}
$abuse = new Abuse($timeLimit);
$remaining = $timeLimit->remaining();
$limit = $timeLimit->limit();
$time = $timeLimit->time() + $route->getLabel('abuse-time', 3600);
if ($limit && ($remaining < $closestLimit || is_null($closestLimit))) {
$closestLimit = $remaining;
$response
->addHeader('X-RateLimit-Limit', $limit)
->addHeader('X-RateLimit-Remaining', $remaining)
->addHeader('X-RateLimit-Reset', $time);
}
if ($shouldCheckAbuse) {
$isRateLimited = $abuse->check();
}
} catch (\Throwable $th) {
\error_log((string) $th);
continue;
}
if ($isRateLimited) {
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED);
}
}
});
Http::init()
->groups(['api'])
->inject('utopia')
@@ -485,22 +564,19 @@ Http::init()
->inject('user')
->inject('queueForEvents')
->inject('auditContext')
->inject('queueForDeletes')
->inject('queueForDatabase')
->inject('usage')
->inject('queueForFunctions')
->inject('publisherForFunctions')
->inject('dbForProject')
->inject('timelimit')
->inject('resourceToken')
->inject('mode')
->inject('apiKey')
->inject('plan')
->inject('devKey')
->inject('telemetry')
->inject('platform')
->inject('authorization')
->inject('cacheControlForStorage')
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Delete $queueForDeletes, EventDatabase $queueForDatabase, Context $usage, Func $queueForFunctions, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization, callable $cacheControlForStorage) {
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, EventDatabase $queueForDatabase, Context $usage, FunctionPublisher $publisherForFunctions, Database $dbForProject, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Telemetry $telemetry, array $platform, Authorization $authorization, callable $cacheControlForStorage) {
$response->setUser($user);
$request->setUser($user);
@@ -517,70 +593,6 @@ Http::init()
default => '',
};
/*
* Abuse Check
*/
$abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}');
$timeLimitArray = [];
$abuseKeyLabel = (! is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel;
foreach ($abuseKeyLabel as $abuseKey) {
$start = $request->getContentRangeStart();
$end = $request->getContentRangeEnd();
$timeLimit = $timelimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600));
$timeLimit
->setParam('{projectId}', $project->getId())
->setParam('{userId}', $user->getId())
->setParam('{userAgent}', $request->getUserAgent(''))
->setParam('{ip}', $request->getIP())
->setParam('{url}', $request->getHostname() . $route->getPath())
->setParam('{method}', $request->getMethod())
->setParam('{chunkId}', (int) ($start / ($end + 1 - $start)));
$timeLimitArray[] = $timeLimit;
}
$closestLimit = null;
$roles = $authorization->getRoles();
$isPrivilegedUser = $user->isPrivileged($roles);
$isAppUser = $user->isApp($roles);
foreach ($timeLimitArray as $timeLimit) {
foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys
if (! empty($value)) {
$timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value);
}
}
$abuse = new Abuse($timeLimit);
$remaining = $timeLimit->remaining();
$limit = $timeLimit->limit();
$time = $timeLimit->time() + $route->getLabel('abuse-time', 3600);
if ($limit && ($remaining < $closestLimit || is_null($closestLimit))) {
$closestLimit = $remaining;
$response
->addHeader('X-RateLimit-Limit', $limit)
->addHeader('X-RateLimit-Remaining', $remaining)
->addHeader('X-RateLimit-Reset', $time);
}
$enabled = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled';
if (
$enabled // Abuse is enabled
&& ! $isAppUser // User is not API key
&& ! $isPrivilegedUser // User is not an admin
&& $devKey->isEmpty() // request doesn't not contain development key
&& $abuse->check() // Route is rate-limited
) {
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED);
}
}
/**
* TODO: (@loks0n)
* Avoid mutating the message across file boundaries - it's difficult to reason about at scale.
@@ -611,19 +623,16 @@ Http::init()
}
/* Auto-set projects */
$queueForDeletes->setProject($project);
$queueForDatabase->setProject($project);
$queueForFunctions->setProject($project);
/* Auto-set platforms */
$queueForFunctions->setPlatform($platform);
$useCache = $route->getLabel('cache', false);
$storageCacheOperationsCounter = $telemetry->createCounter('storage.cache.operations.load');
if ($useCache) {
$route = $utopia->match($request);
$roles = $authorization->getRoles();
$isAppUser = $user->isApp($roles);
$isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview';
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && ! $user->isPrivileged($authorization->getRoles());
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && ! $user->isPrivileged($roles);
$key = $request->cacheIdentifier();
Span::add('storage.cache.key', $key);
@@ -644,7 +653,7 @@ Http::init()
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isToken = ! $resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence();
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
$isPrivilegedUser = $user->isPrivileged($roles);
if ($bucket->isEmpty() || (! $bucket->getAttribute('enabled') && ! $isAppUser && ! $isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@@ -806,9 +815,8 @@ Http::shutdown()
->inject('publisherForAudits')
->inject('usage')
->inject('publisherForUsage')
->inject('queueForDeletes')
->inject('queueForDatabase')
->inject('queueForFunctions')
->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('queueForRealtime')
->inject('dbForProject')
@@ -818,7 +826,7 @@ Http::shutdown()
->inject('bus')
->inject('apiKey')
->inject('mode')
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Audit $publisherForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) {
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Audit $publisherForAudits, Context $usage, UsagePublisher $publisherForUsage, EventDatabase $queueForDatabase, FunctionPublisher $publisherForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) {
$responsePayload = $response->getPayload();
@@ -847,9 +855,15 @@ Http::shutdown()
if (! empty($functionsEvents)) {
foreach ($generatedEvents as $event) {
if (isset($functionsEvents[$event])) {
$queueForFunctions
->from($queueForEvents)
->trigger();
$publisherForFunctions->enqueue(FunctionMessage::fromEvent(
event: $queueForEvents->getEvent(),
params: $queueForEvents->getParams(),
project: $queueForEvents->getProject(),
user: $queueForEvents->getUser(),
userId: $queueForEvents->getUserId(),
payload: $queueForEvents->getPayload(),
platform: $queueForEvents->getPlatform(),
));
break;
}
}
@@ -959,10 +973,6 @@ Http::shutdown()
$publisherForAudits->enqueue(AuditMessage::fromContext($auditContext));
}
if (! empty($queueForDeletes->getType())) {
$queueForDeletes->trigger();
}
if (! empty($queueForDatabase->getType())) {
$queueForDatabase->trigger();
}
+31 -39
View File
@@ -3,7 +3,7 @@
require_once __DIR__ . '/init.php';
require_once __DIR__ . '/init/span.php';
$registerRequestResources = require __DIR__ . '/init/resources/request.php';
$setRequestContext = require __DIR__ . '/init/resources/request.php';
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
@@ -26,6 +26,7 @@ use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\DI\Container;
use Utopia\Http\Adapter\Swoole\Server;
use Utopia\Http\Files;
use Utopia\Http\Http;
@@ -57,7 +58,7 @@ $container->set('pools', function ($register) {
$payloadSize = 12 * (1024 * 1024); // 12MB - adding slight buffer for headers and other data that might be sent with the payload - update later with valid testing
$totalWorkers = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$swooleAdapter = new Server(
$swoole = new Server(
host: "0.0.0.0",
port: System::getEnv('PORT', 80),
settings: [
@@ -69,10 +70,10 @@ $swooleAdapter = new Server(
Constant::OPTION_OUTPUT_BUFFER_SIZE => $payloadSize,
Constant::OPTION_TASK_WORKER_NUM => 1, // required for the task to fetch domains background
],
container: $container,
resources: $container,
);
$http = $swooleAdapter->getServer();
$http = $swoole->getServer();
/**
* Assigns HTTP requests to worker threads by analyzing its payload/content.
@@ -190,13 +191,11 @@ $http->on(Constant::EVENT_AFTER_RELOAD, function ($server) {
Console::success('Reload completed...');
});
$container->set('bus', function ($register) use ($swooleAdapter) {
return $register->get('bus')->setResolver(fn (string $name) => $swooleAdapter->getContainer()->get($name));
}, ['register']);
$container->set('bus', fn ($register) => $register->get('bus')->setResolver(fn (string $name) => $swoole->context()->get($name)), ['register']);
include __DIR__ . '/controllers/general.php';
function createDatabase(Http $app, string $resourceKey, string $dbName, array $collections, mixed $pools, ?callable $extraSetup = null): void
function createDatabase(Container $resources, string $resourceKey, string $dbName, array $collections, mixed $pools, ?callable $extraSetup = null): void
{
$max = 15;
$sleep = 2;
@@ -205,7 +204,7 @@ function createDatabase(Http $app, string $resourceKey, string $dbName, array $c
while (true) {
try {
$attempts++;
$resource = $app->getResource($resourceKey);
$resource = $resources->get($resourceKey);
/* @var $database Database */
$database = is_callable($resource) ? $resource() : $resource;
break; // exit loop on success
@@ -288,23 +287,21 @@ function createDatabase(Http $app, string $resourceKey, string $dbName, array $c
Span::current()?->finish();
}
$http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorkers, $swooleAdapter) {
$app = new Http($swooleAdapter, 'UTC');
$http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorkers, $container) {
/** @var \Utopia\Pools\Group $pools */
$pools = $app->getResource('pools');
$pools = $container->get('pools');
go(function () use ($app, $pools) {
go(function () use ($container, $pools) {
/** @var array $collections */
$collections = Config::getParam('collections', []);
// create logs database first, `getLogsDB` is a callable.
createDatabase($app, 'getLogsDB', 'logs', $collections['logs'], $pools);
createDatabase($container, 'getLogsDB', 'logs', $collections['logs'], $pools);
// create appwrite database, `dbForPlatform` is a direct access call.
createDatabase($app, 'dbForPlatform', 'appwrite', $collections['console'], $pools, function (Database $dbForPlatform) use ($collections, $app) {
$authorization = $app->getResource('authorization');
createDatabase($container, 'dbForPlatform', 'appwrite', $collections['console'], $pools, function (Database $dbForPlatform) use ($collections, $container) {
$authorization = $container->get('authorization');
if ($dbForPlatform->getCollection(AuditAdapterSQL::COLLECTION)->isEmpty()) {
$adapter = new AdapterDatabase($dbForPlatform);
@@ -416,7 +413,7 @@ $http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorke
$documentsSharedTables = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''));
$vectorSharedTables = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''));
$cache = $app->getResource('cache');
$cache = $container->get('cache');
// All shared tables pools that need project metadata collections
$allSharedTables = \array_values(\array_unique(\array_filter([
@@ -502,7 +499,7 @@ $http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorke
});
});
$swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swooleAdapter, $registerRequestResources) {
$swoole->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swoole, $setRequestContext) {
Span::init('http.request');
$request = new Request($utopiaRequest->getSwooleRequest());
@@ -522,21 +519,18 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
return;
}
$requestContainer = $swooleAdapter->getContainer();
$requestContainer->set('container', fn () => $requestContainer);
$requestContainer->set('request', fn () => $request);
$requestContainer->set('response', fn () => $response);
$app = new Http($swoole, 'UTC');
$app->context()->set('request', fn () => $request);
$app->context()->set('response', fn () => $response);
$app->context()->set('utopia', fn () => $app);
$app = new Http($swooleAdapter, 'UTC');
$requestContainer->set('utopia', fn () => $app);
$registerRequestResources($requestContainer);
$setRequestContext($app->context());
$app->setCompression(System::getEnv('_APP_COMPRESSION_ENABLED', 'enabled') === 'enabled');
$app->setCompressionMinSize(intval(System::getEnv('_APP_COMPRESSION_MIN_SIZE_BYTES', '1024'))); // 1KB
try {
$authorization = $app->getResource('authorization');
$authorization = $app->context()->get('authorization');
$request->setAuthorization($authorization);
$response->setAuthorization($authorization);
@@ -552,18 +546,18 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$logger = $app->getResource("logger");
$logger = $app->context()->get("logger");
if ($logger) {
try {
/** @var Utopia\Database\Document $user */
$user = $app->getResource('user');
$user = $app->context()->get('user');
} catch (\Throwable $_th) {
// All good, user is optional information for logger
}
$route = $app->getRoute();
$log = $app->getResource("log");
$log = $app->context()->get("log");
if (isset($user) && !$user->isEmpty()) {
$log->setUser(new User($user->getId()));
@@ -642,18 +636,16 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
});
// Fetch domains every `DOMAIN_SYNC_TIMER` seconds and update in the memory
$http->on(Constant::EVENT_TASK, function () use ($swooleAdapter) {
$http->on(Constant::EVENT_TASK, function () use ($container) {
$lastSyncUpdate = null;
$app = new Http($swooleAdapter, 'UTC');
/** @var Utopia\Database\Database $dbForPlatform */
$dbForPlatform = $app->getResource('dbForPlatform');
$dbForPlatform = $container->get('dbForPlatform');
/** @var \Swoole\Table $riskyDomains */
$riskyDomains = $app->getResource('riskyDomains');
$riskyDomains = $container->get('riskyDomains');
Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $riskyDomains, &$lastSyncUpdate, $app) {
Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $riskyDomains, &$lastSyncUpdate, $container) {
try {
$time = DateTime::now();
$limit = 1000;
@@ -670,7 +662,7 @@ $http->on(Constant::EVENT_TASK, function () use ($swooleAdapter) {
}
$results = [];
try {
$authorization = $app->getResource('authorization');
$authorization = $container->get('authorization');
$results = $authorization->skip(fn () => $dbForPlatform->find('rules', $queries));
} catch (Throwable $th) {
Console::error('rules ' . $th->getMessage());
@@ -720,4 +712,4 @@ $http->on(Constant::EVENT_TASK, function () use ($swooleAdapter) {
});
});
$swooleAdapter->start();
$swoole->start();
+58 -2
View File
@@ -1,5 +1,11 @@
<?php
use Appwrite\Platform\Modules\Advisor\Enums\InsightCTAMethod;
use Appwrite\Platform\Modules\Advisor\Enums\InsightCTAService;
use Appwrite\Platform\Modules\Advisor\Enums\InsightSeverity;
use Appwrite\Platform\Modules\Advisor\Enums\InsightStatus;
use Appwrite\Platform\Modules\Advisor\Enums\InsightType;
use Appwrite\Platform\Modules\Advisor\Enums\ReportType;
use Appwrite\Platform\Modules\Compute\Specification;
use Utopia\System\System;
@@ -44,7 +50,7 @@ const APP_PROJECT_ACCESS = 24 * 60 * 60; // 24 hours
const APP_RESOURCE_TOKEN_ACCESS = 24 * 60 * 60; // 24 hours
const APP_FILE_ACCESS = 24 * 60 * 60; // 24 hours
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
const APP_CACHE_BUSTER = 4325;
const APP_CACHE_BUSTER = 4326;
const APP_VERSION_STABLE = '1.9.5';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
@@ -194,7 +200,7 @@ const BUILD_TYPE_RETRY = 'retry';
const DELETE_TYPE_DATABASES = 'databases';
const DELETE_TYPE_DOCUMENT = 'document';
const DELETE_TYPE_COLLECTIONS = 'collections';
const DELETE_TYPE_TRANSACTION = 'transaction';
const DELETE_TYPE_TRANSACTIONS = 'transactions';
const DELETE_TYPE_EXPIRED_TRANSACTIONS = 'expired_transactions';
const DELETE_TYPE_PROJECTS = 'projects';
const DELETE_TYPE_SITES = 'sites';
@@ -222,6 +228,7 @@ const DELETE_TYPE_EXPIRED_TARGETS = 'invalid_targets';
const DELETE_TYPE_SESSION_TARGETS = 'session_targets';
const DELETE_TYPE_CSV_EXPORTS = 'csv_exports';
const DELETE_TYPE_MAINTENANCE = 'maintenance';
const DELETE_TYPE_REPORT = 'report';
// Rule statuses
const RULE_STATUS_CREATED = 'created'; // This is also the status when domain DNS verification fails.
@@ -424,6 +431,55 @@ const RESOURCE_TYPE_MESSAGES = 'messages';
const RESOURCE_TYPE_EXECUTIONS = 'executions';
const RESOURCE_TYPE_VCS = 'vcs';
const RESOURCE_TYPE_EMBEDDINGS_TEXT = 'embeddingsText';
const RESOURCE_TYPE_INSIGHTS = 'insights';
const RESOURCE_TYPE_REPORTS = 'reports';
// Insight types — engine-specific so the CTA action can reference the right public API.
const ADVISOR_INSIGHT_TYPES = [
InsightType::DATABASE_INDEX->value, // legacy databases.createIndex
InsightType::TABLES_DB_INDEX->value, // tablesDB.createIndex
InsightType::DOCUMENTS_DB_INDEX->value, // documentsDB.createIndex
InsightType::VECTORS_DB_INDEX->value, // vectorsDB.createIndex
InsightType::DATABASE_PERFORMANCE->value,
InsightType::SITE_PERFORMANCE->value,
InsightType::SITE_ACCESSIBILITY->value,
InsightType::SITE_SEO->value,
InsightType::FUNCTION_PERFORMANCE->value,
];
// Public API services (SDK namespaces) that an insight CTA's `service` can reference.
// Analyzers must pick the one matching the engine the resource lives in.
const ADVISOR_CTA_SERVICES = [
InsightCTAService::DATABASES->value, // legacy
InsightCTAService::TABLES_DB->value,
InsightCTAService::DOCUMENTS_DB->value,
InsightCTAService::VECTORS_DB->value,
];
// Public API method names that an insight CTA's `method` can reference for index suggestions.
const ADVISOR_CTA_METHODS = [
InsightCTAMethod::CREATE_INDEX->value,
];
// Insight severities
const ADVISOR_SEVERITIES = [
InsightSeverity::INFO->value,
InsightSeverity::WARNING->value,
InsightSeverity::CRITICAL->value,
];
// Insight statuses
const ADVISOR_STATUSES = [
InsightStatus::ACTIVE->value,
InsightStatus::DISMISSED->value,
];
// Report types
const ADVISOR_REPORT_TYPES = [
ReportType::LIGHTHOUSE->value,
ReportType::AUDIT->value,
ReportType::DATABASE_ANALYZER->value,
];
// Resource types for Tokens
const TOKENS_RESOURCE_TYPE_FILES = 'files';
+14
View File
@@ -475,3 +475,17 @@ Database::addFilter(
]));
}
);
Database::addFilter(
'subQueryReportInsights',
function (mixed $value) {
return;
},
function (mixed $value, Document $document, Database $database) {
return $database->getAuthorization()->skip(fn () => $database->find('insights', [
Query::equal('projectInternalId', [$document->getAttribute('projectInternalId')]),
Query::equal('reportInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
]));
}
);
+8
View File
@@ -92,6 +92,8 @@ use Appwrite\Utopia\Response\Model\HealthTime;
use Appwrite\Utopia\Response\Model\HealthVersion;
use Appwrite\Utopia\Response\Model\Identity;
use Appwrite\Utopia\Response\Model\Index;
use Appwrite\Utopia\Response\Model\Insight;
use Appwrite\Utopia\Response\Model\InsightCTA;
use Appwrite\Utopia\Response\Model\Installation;
use Appwrite\Utopia\Response\Model\JWT;
use Appwrite\Utopia\Response\Model\Key;
@@ -182,6 +184,7 @@ use Appwrite\Utopia\Response\Model\ProviderRepositoryFramework;
use Appwrite\Utopia\Response\Model\ProviderRepositoryFrameworkList;
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntime;
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntimeList;
use Appwrite\Utopia\Response\Model\Report;
use Appwrite\Utopia\Response\Model\ResourceToken;
use Appwrite\Utopia\Response\Model\Row;
use Appwrite\Utopia\Response\Model\Rule;
@@ -291,6 +294,8 @@ Response::setModel(new BaseList('Specifications List', Response::MODEL_SPECIFICA
Response::setModel(new BaseList('VCS Content List', Response::MODEL_VCS_CONTENT_LIST, 'contents', Response::MODEL_VCS_CONTENT));
Response::setModel(new BaseList('VectorsDB Collections List', Response::MODEL_VECTORSDB_COLLECTION_LIST, 'collections', Response::MODEL_VECTORSDB_COLLECTION));
Response::setModel(new BaseList('Embedding list', Response::MODEL_EMBEDDING_LIST, 'embeddings', Response::MODEL_EMBEDDING));
Response::setModel(new BaseList('Insights List', Response::MODEL_INSIGHT_LIST, 'insights', Response::MODEL_INSIGHT));
Response::setModel(new BaseList('Reports List', Response::MODEL_REPORT_LIST, 'reports', Response::MODEL_REPORT));
// Entities
Response::setModel(new Database());
@@ -515,6 +520,9 @@ Response::setModel(new Target());
Response::setModel(new Migration());
Response::setModel(new MigrationReport());
Response::setModel(new MigrationFirebaseProject());
Response::setModel(new Insight());
Response::setModel(new InsightCTA());
Response::setModel(new Report());
// Tests (keep last)
Response::setModel(new Mock());
+10
View File
@@ -4,7 +4,9 @@ use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Audit as AuditPublisher;
use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Event\Publisher\Certificate as CertificatePublisher;
use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Event\Publisher\Execution as ExecutionPublisher;
use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Publisher\Mail as MailPublisher;
use Appwrite\Event\Publisher\Messaging as MessagingPublisher;
use Appwrite\Event\Publisher\Migration as MigrationPublisher;
@@ -108,6 +110,10 @@ $container->set('publisherForExecutions', fn (Publisher $publisher) => new Execu
$publisher,
new Queue(System::getEnv('_APP_EXECUTIONS_QUEUE_NAME', Event::EXECUTIONS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForFunctions', fn (Publisher $publisher) => new FunctionPublisher(
$publisher,
new Queue(System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME), 'utopia-queue', Event::FUNCTIONS_QUEUE_TTL)
), ['publisher']);
$container->set('publisherForMigrations', fn (Publisher $publisher) => new MigrationPublisher(
$publisher,
new Queue(System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME))
@@ -120,6 +126,10 @@ $container->set('publisherForBuilds', fn (Publisher $publisher) => new BuildPubl
$publisher,
new Queue(System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForDeletes', fn (Publisher $publisher) => new DeletePublisher(
$publisher,
new Queue(System::getEnv('_APP_DELETE_QUEUE_NAME', Event::DELETE_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForMails', fn (Publisher $publisher) => new MailPublisher(
$publisher,
new Queue(System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME))
+92 -120
View File
@@ -6,9 +6,9 @@ use Appwrite\Auth\Key;
use Appwrite\Databases\TransactionState;
use Appwrite\Event\Context\Audit as AuditContext;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Message\Func as FunctionMessage;
use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
@@ -48,6 +48,7 @@ use Utopia\Locale\Locale;
use Utopia\Logger\Log;
use Utopia\Pools\Group;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
use Utopia\Storage\Device;
use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
@@ -59,26 +60,18 @@ use Utopia\Validator\WhiteList;
* These resources depend (directly or transitively) on request/response
* and must be fresh for each HTTP request.
*/
return function (Container $container): void {
$container->set('utopia:graphql', function ($utopia) {
return $utopia;
}, ['utopia']);
return function (Container $context): void {
$context->set('utopia:graphql', fn ($utopia) => $utopia, ['utopia']);
$container->set('log', fn () => new Log(), []);
$context->set('log', fn () => new Log(), []);
$container->set('logger', function ($register) {
return $register->get('logger');
}, ['register']);
$context->set('logger', fn ($register) => $register->get('logger'), ['register']);
$container->set('authorization', function () {
return new Authorization();
}, []);
$context->set('authorization', fn () => new Authorization(), []);
$container->set('store', function (): Store {
return new Store();
}, []);
$context->set('store', fn (): Store => new Store(), []);
$container->set('proofForPassword', function (): Password {
$context->set('proofForPassword', function (): Password {
$hash = new Argon2();
$hash
->setMemoryCost(7168)
@@ -92,21 +85,21 @@ return function (Container $container): void {
return $password;
});
$container->set('proofForToken', function (): Token {
$context->set('proofForToken', function (): Token {
$token = new Token();
$token->setHash(new Sha());
return $token;
});
$container->set('proofForCode', function (): Code {
$context->set('proofForCode', function (): Code {
$code = new Code();
$code->setHash(new Sha());
return $code;
});
$container->set('locale', function () {
$context->set('locale', function () {
$locale = new Locale(System::getEnv('_APP_LOCALE', 'en'));
$locale->setFallback(System::getEnv('_APP_LOCALE', 'en'));
@@ -114,32 +107,18 @@ return function (Container $container): void {
});
// Per-request queue resources (stateful, accumulate event data during request)
$container->set('queueForDatabase', function (Publisher $publisher) {
return new EventDatabase($publisher);
}, ['publisher']);
$container->set('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
$container->set('queueForEvents', function (Publisher $publisher) {
return new Event($publisher);
}, ['publisher']);
$container->set('queueForWebhooks', function (Publisher $publisher) {
return new Webhook($publisher);
}, ['publisher']);
$container->set('queueForRealtime', function () {
return new Realtime();
}, []);
$container->set('usage', function () {
return new UsageContext();
}, []);
$container->set('auditContext', fn () => new AuditContext(), []);
$container->set('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
$container->set('eventProcessor', function () {
return new EventProcessor();
}, []);
$container->set('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) {
$context->set('queueForDatabase', fn (Publisher $publisher) => new EventDatabase($publisher), ['publisher']);
$context->set('queueForEvents', fn (Publisher $publisher) => new Event($publisher), ['publisher']);
$context->set('queueForWebhooks', fn (Publisher $publisher) => new Webhook($publisher), ['publisher']);
$context->set('queueForRealtime', fn () => new Realtime(), []);
$context->set('usage', fn () => new UsageContext(), []);
$context->set('auditContext', fn () => new AuditContext(), []);
$context->set('publisherForFunctions', fn (Publisher $publisher) => new FunctionPublisher(
$publisher,
new Queue(System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME), 'utopia-queue', Event::FUNCTIONS_QUEUE_TTL)
), ['publisher']);
$context->set('eventProcessor', fn () => new EventProcessor(), []);
$context->set('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) {
$adapter = new DatabasePool($pools->get('console'));
$database = new Database($adapter, $cache);
@@ -157,7 +136,7 @@ return function (Container $container): void {
return $database;
}, ['pools', 'cache', 'authorization']);
$container->set('getProjectDB', function (Group $pools, Database $dbForPlatform, Cache $cache, Authorization $authorization) {
$context->set('getProjectDB', function (Group $pools, Database $dbForPlatform, Cache $cache, Authorization $authorization) {
$adapters = [];
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$adapters) {
@@ -214,7 +193,7 @@ return function (Container $container): void {
};
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
$container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$context->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$adapter = null;
return function (?Document $project = null) use ($pools, $cache, $authorization, &$adapter) {
@@ -246,7 +225,7 @@ return function (Container $container): void {
/**
* List of allowed request hostnames for the request.
*/
$container->set('allowedHostnames', function (array $platform, Document $project, Document $rule, Document $devKey, Request $request) {
$context->set('allowedHostnames', function (array $platform, Document $project, Document $rule, Document $devKey, Request $request) {
$allowed = [...($platform['hostnames'] ?? [])];
/* Add platform configured hostnames */
@@ -290,7 +269,7 @@ return function (Container $container): void {
/**
* List of allowed request schemes for the request.
*/
$container->set('allowedSchemes', function (array $platform, Document $project) {
$context->set('allowedSchemes', function (array $platform, Document $project) {
$allowed = [...($platform['schemas'] ?? [])];
if (! $project->isEmpty() && $project->getId() !== 'console') {
@@ -310,7 +289,7 @@ return function (Container $container): void {
/**
* Whether the request origin is verified against the request hostname.
*/
$container->set('domainVerification', function (Request $request) {
$context->set('domainVerification', function (Request $request) {
$origin = \parse_url($request->getOrigin($request->getReferer('')), PHP_URL_HOST);
$selfDomain = new Domain($request->getHostname());
$endDomain = new Domain((string) $origin);
@@ -322,7 +301,7 @@ return function (Container $container): void {
/**
* Cookie domain for the current request.
*/
$container->set('cookieDomain', function (Request $request, Document $project) {
$context->set('cookieDomain', function (Request $request, Document $project) {
$localHosts = ['localhost', 'localhost:' . $request->getPort()];
$migrationHost = System::getEnv('_APP_MIGRATION_HOST');
@@ -356,7 +335,7 @@ return function (Container $container): void {
/**
* Rule associated with a request origin.
*/
$container->set('rule', function (Request $request, Database $dbForPlatform, Document $project, Authorization $authorization) {
$context->set('rule', function (Request $request, Database $dbForPlatform, Document $project, Authorization $authorization) {
$domain = \parse_url($request->getOrigin(), PHP_URL_HOST);
if (empty($domain)) {
@@ -406,7 +385,7 @@ return function (Container $container): void {
/**
* CORS service
*/
$container->set('cors', function (array $allowedHostnames) {
$context->set('cors', function (array $allowedHostnames) {
$corsConfig = Config::getParam('cors');
return new Cors(
@@ -418,23 +397,23 @@ return function (Container $container): void {
);
}, ['allowedHostnames']);
$container->set('originValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) {
if (! $devKey->isEmpty()) {
return new URL();
}
$context->set(
'originValidator',
fn (Document $devKey, array $allowedHostnames, array $allowedSchemes) => $devKey->isEmpty()
? new Origin($allowedHostnames, $allowedSchemes)
: new URL(),
['devKey', 'allowedHostnames', 'allowedSchemes']
);
return new Origin($allowedHostnames, $allowedSchemes);
}, ['devKey', 'allowedHostnames', 'allowedSchemes']);
$context->set(
'redirectValidator',
fn (Document $devKey, array $allowedHostnames, array $allowedSchemes) => $devKey->isEmpty()
? new Redirect($allowedHostnames, $allowedSchemes)
: new URL(),
['devKey', 'allowedHostnames', 'allowedSchemes']
);
$container->set('redirectValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) {
if (! $devKey->isEmpty()) {
return new URL();
}
return new Redirect($allowedHostnames, $allowedSchemes);
}, ['devKey', 'allowedHostnames', 'allowedSchemes']);
$container->set('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken, $authorization) {
$context->set('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken, $authorization) {
/**
* Handles user authentication and session validation.
*
@@ -605,7 +584,7 @@ return function (Container $container): void {
return $user;
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken', 'authorization']);
$container->set('project', function ($dbForPlatform, $request, $console, $authorization, Http $utopia) {
$context->set('project', function ($dbForPlatform, $request, $console, $authorization, Http $utopia) {
/** @var Appwrite\Utopia\Request $request */
/** @var Utopia\Database\Database $dbForPlatform */
/** @var Utopia\Database\Document $console */
@@ -638,7 +617,7 @@ return function (Container $container): void {
return $project;
}, ['dbForPlatform', 'request', 'console', 'authorization', 'utopia']);
$container->set('session', function (User $user, Store $store, Token $proofForToken) {
$context->set('session', function (User $user, Store $store, Token $proofForToken) {
if ($user->isEmpty()) {
return;
}
@@ -659,7 +638,7 @@ return function (Container $container): void {
return;
}, ['user', 'store', 'proofForToken']);
$container->set('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, UsageContext $usage, Authorization $authorization, Request $request) {
$context->set('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, FunctionPublisher $publisherForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, UsageContext $usage, Authorization $authorization, Request $request) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
@@ -715,7 +694,7 @@ return function (Container $container): void {
* Accounts can be created in many ways beyond `createAccount`
* (anonymous, OAuth, phone, etc.), and those flows are probably not covered in event tests; so we handle this here.
*/
$eventDatabaseListener = function (Document $project, Document $document, Response $response, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime) {
$eventDatabaseListener = function (Document $project, Document $document, Response $response, Event $queueForEvents, FunctionPublisher $publisherForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime) {
// Only trigger events for user creation with the database listener.
if ($document->getCollection() !== 'users') {
return;
@@ -727,9 +706,15 @@ return function (Container $container): void {
->setPayload($response->output($document, Response::MODEL_USER));
// Trigger functions, webhooks, and realtime events
$queueForFunctions
->from($queueForEvents)
->trigger();
$publisherForFunctions->enqueue(FunctionMessage::fromEvent(
event: $queueForEvents->getEvent(),
params: $queueForEvents->getParams(),
project: $queueForEvents->getProject(),
user: $queueForEvents->getUser(),
userId: $queueForEvents->getUserId(),
payload: $queueForEvents->getPayload(),
platform: $queueForEvents->getPlatform(),
));
/** Trigger webhooks events only if a project has them enabled */
if (! empty($project->getAttribute('webhooks'))) {
@@ -909,7 +894,6 @@ return function (Container $container): void {
// Clone the queues, to prevent events triggered by the database listener
// from overwriting the events that are supposed to be triggered in the shutdown hook.
$queueForEventsClone = new Event($publisher);
$queueForFunctions = new Func($publisherFunctions);
$queueForWebhooks = new Webhook($publisherWebhooks);
$queueForRealtime = new Realtime();
@@ -924,7 +908,7 @@ return function (Container $container): void {
$document,
$response,
$queueForEventsClone->from($queueForEvents),
$queueForFunctions->from($queueForEvents),
$publisherForFunctions,
$queueForWebhooks->from($queueForEvents),
$queueForRealtime->from($queueForEvents)
))
@@ -933,9 +917,9 @@ return function (Container $container): void {
->on(Database::EVENT_DOCUMENT_DELETE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database));
return $database;
}, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'queueForFunctions', 'queueForWebhooks', 'queueForRealtime', 'usage', 'authorization', 'request']);
}, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'publisherForFunctions', 'queueForWebhooks', 'queueForRealtime', 'usage', 'authorization', 'request']);
$container->set('schema', function ($utopia, $dbForProject, $authorization) {
$context->set('schema', function ($utopia, $dbForProject, $authorization) {
$complexity = function (int $complexity, array $args) {
$queries = Query::parseQueries($args['queries'] ?? []);
@@ -1022,13 +1006,9 @@ return function (Container $container): void {
);
}, ['utopia', 'dbForProject', 'authorization']);
$container->set('audit', function ($dbForProject) {
$adapter = new AdapterDatabase($dbForProject);
$context->set('audit', fn ($dbForProject) => new Audit(new AdapterDatabase($dbForProject)), ['dbForProject']);
return new Audit($adapter);
}, ['dbForProject']);
$container->set('mode', function ($request, Document $project) {
$context->set('mode', function ($request, Document $project) {
/** @var Appwrite\Utopia\Request $request */
/**
@@ -1046,7 +1026,7 @@ return function (Container $container): void {
return $mode;
}, ['request', 'project']);
$container->set('requestTimestamp', function ($request) {
$context->set('requestTimestamp', function ($request) {
// TODO: Move this to the Request class itself
$timestampHeader = $request->getHeader('x-appwrite-timestamp');
$requestTimestamp = null;
@@ -1061,7 +1041,7 @@ return function (Container $container): void {
return $requestTimestamp;
}, ['request']);
$container->set('devKey', function (Request $request, Document $project, array $servers, Database $dbForPlatform, Authorization $authorization) {
$context->set('devKey', function (Request $request, Document $project, array $servers, Database $dbForPlatform, Authorization $authorization) {
$devKey = $request->getHeader('x-appwrite-dev-key', $request->getParam('devKey', ''));
// Check if given key match project's development keys
@@ -1110,7 +1090,7 @@ return function (Container $container): void {
return $key;
}, ['request', 'project', 'servers', 'dbForPlatform', 'authorization']);
$container->set('team', function (Document $project, Database $dbForPlatform, Http $utopia, Request $request, Authorization $authorization) {
$context->set('team', function (Document $project, Database $dbForPlatform, Http $utopia, Request $request, Authorization $authorization) {
$teamInternalId = '';
if ($project->getId() !== 'console') {
$teamInternalId = $project->getAttribute('teamInternalId', '');
@@ -1153,7 +1133,7 @@ return function (Container $container): void {
return $team;
}, ['project', 'dbForPlatform', 'utopia', 'request', 'authorization']);
$container->set('previewHostname', function (Request $request, ?Key $apiKey) {
$context->set('previewHostname', function (Request $request, ?Key $apiKey) {
$allowed = false;
if (Http::isDevelopment()) {
@@ -1172,7 +1152,7 @@ return function (Container $container): void {
return '';
}, ['request', 'apiKey']);
$container->set('apiKey', function (Request $request, Document $project, Document $team, Document $user): ?Key {
$context->set('apiKey', function (Request $request, Document $project, Document $team, Document $user): ?Key {
$key = $request->getHeader('x-appwrite-key');
if (empty($key)) {
@@ -1206,7 +1186,7 @@ return function (Container $container): void {
return $key;
}, ['request', 'project', 'team', 'user']);
$container->set('resourceToken', function ($project, $dbForProject, $request, Authorization $authorization) {
$context->set('resourceToken', function ($project, $dbForProject, $request, Authorization $authorization) {
$tokenJWT = $request->getParam('token');
if (! empty($tokenJWT) && ! $project->isEmpty()) { // JWT authentication
@@ -1273,10 +1253,10 @@ return function (Container $container): void {
return new Document([]);
}, ['project', 'dbForProject', 'request', 'authorization']);
$container->set('getDatabasesDB', function (Group $pools, Cache $cache, Document $project, Request $request, UsageContext $usage, Authorization $authorization) {
$context->set('getDatabasesDB', function (Group $pools, Cache $cache, Document $project, Request $request, UsageContext $usage, Authorization $authorization) {
return function (Document $database) use ($pools, $cache, $project, $request, $usage, $authorization): Database {
$databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
$databaseDSN = $database->getAttribute('database') ?: $project->getAttribute('database', '');
$databaseType = $database->getAttribute('type', '');
try {
@@ -1435,35 +1415,27 @@ return function (Container $container): void {
}, ['pools', 'cache', 'project', 'request', 'usage', 'authorization']);
$container->set('transactionState', function (Database $dbForProject, Authorization $authorization, callable $getDatabasesDB) {
return new TransactionState($dbForProject, $authorization, $getDatabasesDB);
}, ['dbForProject', 'authorization', 'getDatabasesDB']);
$context->set(
'transactionState',
fn (Database $dbForProject, Authorization $authorization, callable $getDatabasesDB) => new TransactionState($dbForProject, $authorization, $getDatabasesDB),
['dbForProject', 'authorization', 'getDatabasesDB']
);
$container->set('executionsRetentionCount', function (Document $project, array $plan) {
if ($project->getId() === 'console' || empty($plan)) {
return 0;
}
$context->set(
'executionsRetentionCount',
fn (Document $project, array $plan) => ($project->getId() === 'console' || empty($plan))
? 0
: (int) ($plan['executionsRetentionCount'] ?? 100),
['project', 'plan']
);
return (int) ($plan['executionsRetentionCount'] ?? 100);
}, ['project', 'plan']);
$context->set('deviceForFiles', fn ($project, Telemetry $telemetry) => new Device\Telemetry($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId())), ['project', 'telemetry']);
$context->set('deviceForSites', fn ($project, Telemetry $telemetry) => new Device\Telemetry($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId())), ['project', 'telemetry']);
$context->set('deviceForMigrations', fn ($project, Telemetry $telemetry) => new Device\Telemetry($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId())), ['project', 'telemetry']);
$context->set('deviceForFunctions', fn ($project, Telemetry $telemetry) => new Device\Telemetry($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId())), ['project', 'telemetry']);
$context->set('deviceForBuilds', fn ($project, Telemetry $telemetry) => new Device\Telemetry($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId())), ['project', 'telemetry']);
$container->set('deviceForFiles', function ($project, Telemetry $telemetry) {
return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForSites', function ($project, Telemetry $telemetry) {
return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForMigrations', function ($project, Telemetry $telemetry) {
return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForFunctions', function ($project, Telemetry $telemetry) {
return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForBuilds', function ($project, Telemetry $telemetry) {
return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('embeddingAgent', function ($register) {
$context->set('embeddingAgent', function ($register) {
$adapter = new Ollama();
$adapter->setEndpoint(System::getEnv('_APP_EMBEDDING_ENDPOINT', 'http://ollama:11434/api/embed'));
$adapter->setTimeout((int) System::getEnv('_APP_EMBEDDING_TIMEOUT', '30000'));
+20 -1
View File
@@ -3,11 +3,30 @@
use Utopia\Span\Exporter;
use Utopia\Span\Span;
use Utopia\Span\Storage;
use Utopia\System\System;
Span::setStorage(new Storage\Coroutine());
Span::addExporter(new Exporter\Pretty(), function (Span $span): bool {
// Resolve trace filters once at boot to avoid repeated env lookups per span.
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
$traceEnabled = $traceProjectId !== '' || $traceFunctionId !== '';
Span::addExporter(new Exporter\Pretty(), function (Span $span) use ($traceEnabled, $traceProjectId, $traceFunctionId): bool {
if (\str_starts_with($span->getAction(), 'listener.')) {
return $span->getError() !== null;
}
// Selective tracing: when _APP_TRACE_PROJECT_ID / _APP_TRACE_FUNCTION_ID are set,
// only export spans tagged with matching project.id / function.id.
if ($traceEnabled) {
if ($traceProjectId !== '' && $span->get('project.id') !== $traceProjectId) {
return false;
}
if ($traceFunctionId !== '' && $span->get('function.id') !== $traceFunctionId) {
return false;
}
}
return true;
});
+6 -10
View File
@@ -1,9 +1,8 @@
<?php
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
use Appwrite\Usage\Context;
@@ -23,6 +22,7 @@ use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\Pools\Group;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
use Utopia\Registry\Registry;
use Utopia\Storage\Device\Telemetry as TelemetryDevice;
use Utopia\System\System;
@@ -331,10 +331,6 @@ return function (Container $container): void {
return new EventDatabase($publisher);
}, ['publisher']);
$container->set('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
$container->set('queueForEvents', function (Publisher $publisher) {
return new Event($publisher);
}, ['publisher']);
@@ -343,10 +339,10 @@ return function (Container $container): void {
return new Webhook($publisher);
}, ['publisher']);
$container->set('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
$container->set('publisherForFunctions', fn (Publisher $publisher) => new FunctionPublisher(
$publisher,
new Queue(System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME), 'utopia-queue', Event::FUNCTIONS_QUEUE_TTL)
), ['publisher']);
$container->set('queueForRealtime', function () {
return new Realtime();
}, []);
+25 -25
View File
@@ -728,8 +728,8 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$success = false;
Span::init('realtime.open');
Span::add('realtime.connectionId', $connection);
Span::add('realtime.inboundBytes', $rawSize);
Span::add('realtime.connection.id', $connection);
Span::add('realtime.inbound_bytes', $rawSize);
if (!empty($request->getOrigin())) {
Span::add('realtime.origin', $request->getOrigin());
}
@@ -936,16 +936,16 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
Span::error($th);
} finally {
Span::add('realtime.success', $success);
Span::add('realtime.responseCode', $responseCode);
Span::add('realtime.subscriptionMode', $subscriptionMode);
Span::add('realtime.channelCount', $channelCount);
Span::add('realtime.subscriptionCount', $subscriptionCount);
Span::add('realtime.outboundBytes', $outboundBytes);
Span::add('realtime.response_code', $responseCode);
Span::add('realtime.subscription_mode', $subscriptionMode);
Span::add('realtime.channel_count', $channelCount);
Span::add('realtime.subscription_count', $subscriptionCount);
Span::add('realtime.outbound_bytes', $outboundBytes);
if (!empty($project?->getId())) {
Span::add('realtime.projectId', $project->getId());
Span::add('project.id', $project->getId());
}
if (!empty($logUser?->getId())) {
Span::add('realtime.userId', $logUser->getId());
Span::add('user.id', $logUser->getId());
}
Span::current()?->finish();
}
@@ -965,9 +965,9 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
$success = false;
Span::init('realtime.message');
Span::add('realtime.connectionId', $connection);
Span::add('realtime.inboundBytes', $rawSize);
Span::add('realtime.containerId', $containerId);
Span::add('realtime.connection.id', $connection);
Span::add('realtime.inbound_bytes', $rawSize);
Span::add('realtime.container.id', $containerId);
try {
$response = new Response(new SwooleResponse());
@@ -1352,15 +1352,15 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
Span::error($th);
} finally {
Span::add('realtime.success', $success);
Span::add('realtime.responseCode', $responseCode);
Span::add('realtime.subscriptionDelta', $subscriptionDelta);
Span::add('realtime.subscriptionsRequested', $subscriptionsRequested);
Span::add('realtime.subscriptionsRemoved', $subscriptionsRemoved);
Span::add('realtime.subscribe.subscriptionsCount', $subscriptionsRequested);
Span::add('realtime.outboundBytes', $outboundBytes);
Span::add('realtime.projectId', $project?->getId() ?? $projectId);
Span::add('realtime.userId', $realtime->connections[$connection]['userId'] ?? null);
Span::add('realtime.messageType', $messageType);
Span::add('realtime.response_code', $responseCode);
Span::add('realtime.subscription_delta', $subscriptionDelta);
Span::add('realtime.subscriptions_requested', $subscriptionsRequested);
Span::add('realtime.subscriptions_removed', $subscriptionsRemoved);
Span::add('realtime.subscribe.subscriptions_count', $subscriptionsRequested);
Span::add('realtime.outbound_bytes', $outboundBytes);
Span::add('project.id', $project?->getId() ?? $projectId);
Span::add('user.id', $realtime->connections[$connection]['userId'] ?? null);
Span::add('realtime.message_type', $messageType);
Span::current()?->finish();
}
});
@@ -1372,7 +1372,7 @@ $server->onClose(function (int $connection) use ($realtime, $stats, $register) {
$success = false;
Span::init('realtime.close');
Span::add('realtime.connectionId', $connection);
Span::add('realtime.connection.id', $connection);
if (array_key_exists($connection, $realtime->connections)) {
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
@@ -1411,12 +1411,12 @@ $server->onClose(function (int $connection) use ($realtime, $stats, $register) {
Span::add('realtime.success', $success);
if (!empty($projectId)) {
Span::add('realtime.projectId', $projectId);
Span::add('project.id', $projectId);
}
if (!empty($userId)) {
Span::add('realtime.userId', $userId);
Span::add('user.id', $userId);
}
Span::add('realtime.subscriptionsBeforeClose', $subscriptionsBeforeClose);
Span::add('realtime.subscriptions_before_close', $subscriptionsBeforeClose);
Span::current()?->finish();
}
+9 -1
View File
@@ -16,6 +16,7 @@ use Utopia\Pools\Group;
use Utopia\Queue\Adapter\Swoole;
use Utopia\Queue\Broker\Pool as BrokerPool;
use Utopia\Queue\Server;
use Utopia\Span\Span;
use Utopia\System\System;
Runtime::enableCoroutine();
@@ -91,8 +92,13 @@ $adapter = new Swoole(
$worker = new Server($adapter, $container);
try {
$worker->init()->action(function () use ($worker, $registerWorkerMessageResources) {
$worker->init()->action(function () use ($worker, $registerWorkerMessageResources, $queueName) {
$registerWorkerMessageResources($worker->getContainer());
Span::init("worker.{$queueName}");
});
$worker->shutdown()->action(function () {
Span::current()?->finish();
});
$container->set('bus', function ($register) use ($worker) {
@@ -120,6 +126,8 @@ $worker
->action(function (Throwable $error, ?Logger $logger, Log $log, Document $project, Authorization $authorization) use ($queueName) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
Span::error($error);
if ($logger) {
$log->setNamespace('appwrite-worker');
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));