mirror of
https://github.com/appwrite/appwrite
synced 2026-05-23 00:49:02 +00:00
Merge branch '1.7.x' into feat-speed-up-site-tests
This commit is contained in:
commit
e7c0111229
20 changed files with 207 additions and 298 deletions
|
|
@ -251,7 +251,7 @@ CLI::setResource('logError', function (Registry $register) {
|
|||
};
|
||||
}, ['register']);
|
||||
|
||||
CLI::setResource('executor', fn () => new Executor(fn (string $projectId, string $deploymentId) => System::getEnv('_APP_EXECUTOR_HOST')));
|
||||
CLI::setResource('executor', fn () => new Executor());
|
||||
|
||||
CLI::setResource('telemetry', fn () => new NoTelemetry());
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ return [
|
|||
[
|
||||
'key' => 'flutter',
|
||||
'name' => 'Flutter',
|
||||
'version' => '16.1.0',
|
||||
'version' => '17.0.1',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-flutter',
|
||||
'package' => 'https://pub.dev/packages/appwrite',
|
||||
'enabled' => true,
|
||||
|
|
@ -77,7 +77,7 @@ return [
|
|||
[
|
||||
'key' => 'apple',
|
||||
'name' => 'Apple',
|
||||
'version' => '10.1.0',
|
||||
'version' => '10.1.1',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-apple',
|
||||
'package' => 'https://github.com/appwrite/sdk-for-apple',
|
||||
'enabled' => true,
|
||||
|
|
@ -217,7 +217,7 @@ return [
|
|||
[
|
||||
'key' => 'cli',
|
||||
'name' => 'Command Line',
|
||||
'version' => '7.0.0',
|
||||
'version' => '8.0.0',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-cli',
|
||||
'package' => 'https://www.npmjs.com/package/appwrite-cli',
|
||||
'enabled' => true,
|
||||
|
|
@ -231,6 +231,11 @@ return [
|
|||
'gitRepoName' => 'sdk-for-cli',
|
||||
'gitUserName' => 'appwrite',
|
||||
'gitBranch' => 'dev',
|
||||
'exclude' => [
|
||||
'services' => [
|
||||
['name' => 'assistant'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
|
@ -411,7 +416,7 @@ return [
|
|||
[
|
||||
'key' => 'swift',
|
||||
'name' => 'Swift',
|
||||
'version' => '10.0.0',
|
||||
'version' => '10.1.0',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-swift',
|
||||
'package' => 'https://github.com/appwrite/sdk-for-swift',
|
||||
'enabled' => true,
|
||||
|
|
|
|||
|
|
@ -25446,12 +25446,33 @@
|
|||
"Temporary Redirect 307",
|
||||
"Permanent Redirect 308"
|
||||
]
|
||||
},
|
||||
"resourceId": {
|
||||
"type": "string",
|
||||
"description": "ID of parent resource.",
|
||||
"x-example": "<RESOURCE_ID>"
|
||||
},
|
||||
"resourceType": {
|
||||
"type": "string",
|
||||
"description": "Type of parent resource.",
|
||||
"x-example": "site",
|
||||
"enum": [
|
||||
"site",
|
||||
"function"
|
||||
],
|
||||
"x-enum-name": "ProxyResourceType",
|
||||
"x-enum-keys": [
|
||||
"Site",
|
||||
"Function"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"domain",
|
||||
"url",
|
||||
"statusCode"
|
||||
"statusCode",
|
||||
"resourceId",
|
||||
"resourceType"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25696,12 +25696,35 @@
|
|||
"Temporary Redirect 307",
|
||||
"Permanent Redirect 308"
|
||||
]
|
||||
},
|
||||
"resourceId": {
|
||||
"type": "string",
|
||||
"description": "ID of parent resource.",
|
||||
"default": null,
|
||||
"x-example": "<RESOURCE_ID>"
|
||||
},
|
||||
"resourceType": {
|
||||
"type": "string",
|
||||
"description": "Type of parent resource.",
|
||||
"default": null,
|
||||
"x-example": "site",
|
||||
"enum": [
|
||||
"site",
|
||||
"function"
|
||||
],
|
||||
"x-enum-name": "ProxyResourceType",
|
||||
"x-enum-keys": [
|
||||
"Site",
|
||||
"Function"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"domain",
|
||||
"url",
|
||||
"statusCode"
|
||||
"statusCode",
|
||||
"resourceId",
|
||||
"resourceType"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -845,15 +845,18 @@ App::get('/v1/health/storage')
|
|||
$checkStart = \microtime(true);
|
||||
|
||||
foreach ($devices as $device) {
|
||||
if (!$device->write($device->getPath('health.txt'), 'test', 'text/plain')) {
|
||||
$uniqueFileName = \uniqid('health', true);
|
||||
$filePath = $device->getPath($uniqueFileName);
|
||||
|
||||
if (!$device->write($filePath, 'test', 'text/plain')) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed writing test file to ' . $device->getRoot());
|
||||
}
|
||||
|
||||
if ($device->read($device->getPath('health.txt')) !== 'test') {
|
||||
if ($device->read($filePath) !== 'test') {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed reading test file from ' . $device->getRoot());
|
||||
}
|
||||
|
||||
if (!$device->delete($device->getPath('health.txt'))) {
|
||||
if (!$device->delete($filePath)) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed deleting test file from ' . $device->getRoot());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -880,7 +880,7 @@ App::setResource('apiKey', function (Request $request, Document $project): ?Key
|
|||
return Key::decode($project, $key);
|
||||
}, ['request', 'project']);
|
||||
|
||||
App::setResource('executor', fn () => new Executor(fn (string $projectId, string $deploymentId) => System::getEnv('_APP_EXECUTOR_HOST')));
|
||||
App::setResource('executor', fn () => new Executor());
|
||||
|
||||
App::setResource('resourceToken', function ($project, $dbForProject, $request) {
|
||||
$tokenJWT = $request->getParam('token');
|
||||
|
|
|
|||
|
|
@ -402,7 +402,7 @@ Server::setResource('logError', function (Registry $register, Document $project)
|
|||
};
|
||||
}, ['register', 'project']);
|
||||
|
||||
Server::setResource('executor', fn () => new Executor(fn (string $projectId, string $deploymentId) => System::getEnv('_APP_EXECUTOR_HOST')));
|
||||
Server::setResource('executor', fn () => new Executor());
|
||||
|
||||
$pools = $register->get('pools');
|
||||
$platform = new Appwrite();
|
||||
|
|
|
|||
12
composer.lock
generated
12
composer.lock
generated
|
|
@ -4807,16 +4807,16 @@
|
|||
"packages-dev": [
|
||||
{
|
||||
"name": "appwrite/sdk-generator",
|
||||
"version": "0.41.1",
|
||||
"version": "0.41.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/sdk-generator.git",
|
||||
"reference": "6d9318abf4542a757c87abf056557d6afa1dc06b"
|
||||
"reference": "07804269131f411576aac60c795a5ebc3afaa48a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/6d9318abf4542a757c87abf056557d6afa1dc06b",
|
||||
"reference": "6d9318abf4542a757c87abf056557d6afa1dc06b",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/07804269131f411576aac60c795a5ebc3afaa48a",
|
||||
"reference": "07804269131f411576aac60c795a5ebc3afaa48a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -4852,9 +4852,9 @@
|
|||
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
|
||||
"support": {
|
||||
"issues": "https://github.com/appwrite/sdk-generator/issues",
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/0.41.1"
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/0.41.4"
|
||||
},
|
||||
"time": "2025-06-01T04:20:04+00:00"
|
||||
"time": "2025-06-10T08:28:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/annotations",
|
||||
|
|
|
|||
|
|
@ -105,9 +105,11 @@ class Get extends Action
|
|||
|
||||
$response
|
||||
->setContentType('application/gzip')
|
||||
->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days
|
||||
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
|
||||
->addHeader('Expires', '0')
|
||||
->addHeader('Pragma', 'no-cache')
|
||||
->addHeader('X-Peak', \memory_get_peak_usage())
|
||||
->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '.tar.gz"');
|
||||
->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '-' . $type . '.tar.gz"');
|
||||
|
||||
$size = $device->getFileSize($path);
|
||||
$rangeHeader = $request->getHeader('range');
|
||||
|
|
|
|||
|
|
@ -877,6 +877,10 @@ class Builds extends Action
|
|||
}
|
||||
}
|
||||
|
||||
$deployment->setAttribute('buildLogs', $logs);
|
||||
|
||||
$this->afterBuildSuccess($dbForProject, $deployment);
|
||||
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
|
||||
|
||||
$queueForRealtime
|
||||
|
|
@ -1313,6 +1317,19 @@ class Builds extends Action
|
|||
->trigger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to run after build success
|
||||
*
|
||||
* @param Database $dbForProject
|
||||
* @param Document $deployment
|
||||
* @return void
|
||||
*/
|
||||
protected function afterBuildSuccess(Database $dbForProject, Document &$deployment): void
|
||||
{
|
||||
assert($dbForProject instanceof Database);
|
||||
assert($deployment instanceof Document);
|
||||
}
|
||||
|
||||
protected function getRuntime(Document $resource, string $version): array
|
||||
{
|
||||
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ use Utopia\Database\Database;
|
|||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Domains\Domain;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
|
|
@ -65,15 +66,18 @@ class Create extends Action
|
|||
->param('domain', null, new ValidatorDomain(), 'Domain name.')
|
||||
->param('url', null, new URL(), 'Target URL of redirection')
|
||||
->param('statusCode', null, new WhiteList([301, 302, 307, 308]), 'Status code of redirection')
|
||||
->param('resourceId', '', new UID(), 'ID of parent resource.')
|
||||
->param('resourceType', '', new WhiteList(['site', 'function']), 'Type of parent resource.')
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('queueForCertificates')
|
||||
->inject('queueForEvents')
|
||||
->inject('dbForPlatform')
|
||||
->inject('dbForProject')
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(string $domain, string $url, int $statusCode, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform)
|
||||
public function action(string $domain, string $url, int $statusCode, string $resourceId, string $resourceType, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject)
|
||||
{
|
||||
$deniedDomains = [
|
||||
'localhost',
|
||||
|
|
@ -116,6 +120,15 @@ class Create extends Action
|
|||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.');
|
||||
}
|
||||
|
||||
$collection = match ($resourceType) {
|
||||
'site' => 'sites',
|
||||
'function' => 'functions'
|
||||
};
|
||||
$resource = $dbForProject->getDocument($collection, $resourceId);
|
||||
if ($resource->isEmpty()) {
|
||||
throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND);
|
||||
}
|
||||
|
||||
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
|
||||
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique();
|
||||
|
||||
|
|
@ -164,6 +177,9 @@ class Create extends Action
|
|||
'trigger' => 'manual',
|
||||
'redirectUrl' => $url,
|
||||
'redirectStatusCode' => $statusCode,
|
||||
'deploymentResourceType' => $resourceType,
|
||||
'deploymentResourceId' => $resource->getId(),
|
||||
'deploymentResourceInternalId' => $resource->getInternalId(),
|
||||
'certificateId' => '',
|
||||
'search' => implode(' ', [$ruleId, $domain->get()]),
|
||||
'owner' => $owner,
|
||||
|
|
|
|||
|
|
@ -99,12 +99,14 @@ class Get extends Action
|
|||
}
|
||||
|
||||
if (!$device->exists($path)) {
|
||||
throw new Exception(Exception::BUILD_NOT_FOUND);
|
||||
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response
|
||||
->setContentType('application/gzip')
|
||||
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache
|
||||
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
|
||||
->addHeader('Expires', '0')
|
||||
->addHeader('Pragma', 'no-cache')
|
||||
->addHeader('X-Peak', \memory_get_peak_usage())
|
||||
->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '-' . $type . '.tar.gz"');
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@ namespace Appwrite\Platform\Tasks;
|
|||
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Delete;
|
||||
use DateInterval;
|
||||
use DateTime;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\DateTime as DatabaseDateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Platform\Action;
|
||||
|
|
@ -58,29 +60,35 @@ class Maintenance extends Action
|
|||
Console::info('Setting loop start time to ' . $next->format("Y-m-d H:i:s.v") . '. Delaying for ' . $delay . ' seconds.');
|
||||
|
||||
Console::loop(function () use ($interval, $cacheRetention, $schedulesDeletionRetention, $usageStatsRetentionHourly, $dbForPlatform, $console, $queueForDeletes, $queueForCertificates) {
|
||||
$time = DateTime::now();
|
||||
$time = DatabaseDateTime::now();
|
||||
|
||||
Console::info("[{$time}] Notifying workers with maintenance tasks every {$interval} seconds");
|
||||
|
||||
// Iterate through project only if it was accessed in last 30 days
|
||||
$dateInterval = DateInterval::createFromDateString('30 days');
|
||||
$before30days = (new DateTime())->sub($dateInterval);
|
||||
|
||||
$dbForPlatform->foreach(
|
||||
'projects',
|
||||
function (Document $project) use ($queueForDeletes, $usageStatsRetentionHourly) {
|
||||
$queueForDeletes
|
||||
->setType(DELETE_TYPE_MAINTENANCE)
|
||||
->setProject($project)
|
||||
->setUsageRetentionHourlyDateTime(DateTime::addSeconds(new \DateTime(), -1 * $usageStatsRetentionHourly))
|
||||
->setUsageRetentionHourlyDateTime(DatabaseDateTime::addSeconds(new \DateTime(), -1 * $usageStatsRetentionHourly))
|
||||
->trigger();
|
||||
},
|
||||
[
|
||||
Query::equal('region', [System::getEnv('_APP_REGION', 'default')]),
|
||||
Query::limit(100),
|
||||
Query::greaterThanEqual('accessedAt', DatabaseDateTime::format($before30days)),
|
||||
Query::orderAsc('teamInternalId'),
|
||||
]
|
||||
);
|
||||
|
||||
$queueForDeletes
|
||||
->setType(DELETE_TYPE_MAINTENANCE)
|
||||
->setProject($console)
|
||||
->setUsageRetentionHourlyDateTime(DateTime::addSeconds(new \DateTime(), -1 * $usageStatsRetentionHourly))
|
||||
->setUsageRetentionHourlyDateTime(DatabaseDateTime::addSeconds(new \DateTime(), -1 * $usageStatsRetentionHourly))
|
||||
->trigger();
|
||||
|
||||
$this->notifyDeleteConnections($queueForDeletes);
|
||||
|
|
@ -94,13 +102,13 @@ class Maintenance extends Action
|
|||
{
|
||||
$queueForDeletes
|
||||
->setType(DELETE_TYPE_REALTIME)
|
||||
->setDatetime(DateTime::addSeconds(new \DateTime(), -60))
|
||||
->setDatetime(DatabaseDateTime::addSeconds(new \DateTime(), -60))
|
||||
->trigger();
|
||||
}
|
||||
|
||||
private function renewCertificates(Database $dbForPlatform, Certificate $queueForCertificate): void
|
||||
{
|
||||
$time = DateTime::now();
|
||||
$time = DatabaseDateTime::now();
|
||||
|
||||
$certificates = $dbForPlatform->find('certificates', [
|
||||
Query::lessThan('attempts', 5), // Maximum 5 attempts
|
||||
|
|
@ -129,7 +137,7 @@ class Maintenance extends Action
|
|||
{
|
||||
$queueForDeletes
|
||||
->setType(DELETE_TYPE_CACHE_BY_TIMESTAMP)
|
||||
->setDatetime(DateTime::addSeconds(new \DateTime(), -1 * $interval))
|
||||
->setDatetime(DatabaseDateTime::addSeconds(new \DateTime(), -1 * $interval))
|
||||
->trigger();
|
||||
}
|
||||
|
||||
|
|
@ -137,7 +145,7 @@ class Maintenance extends Action
|
|||
{
|
||||
$queueForDeletes
|
||||
->setType(DELETE_TYPE_SCHEDULES)
|
||||
->setDatetime(DateTime::addSeconds(new \DateTime(), -1 * $interval))
|
||||
->setDatetime(DatabaseDateTime::addSeconds(new \DateTime(), -1 * $interval))
|
||||
->trigger();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -251,7 +251,8 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|||
->setDiscord(APP_SOCIAL_DISCORD_CHANNEL, APP_SOCIAL_DISCORD)
|
||||
->setDefaultHeaders([
|
||||
'X-Appwrite-Response-Format' => '1.7.0',
|
||||
]);
|
||||
])
|
||||
->setExclude($language['exclude'] ?? []);
|
||||
|
||||
// Make sure we have a clean slate.
|
||||
// Otherwise, all files in this dir will be pushed,
|
||||
|
|
|
|||
|
|
@ -12,13 +12,13 @@ use Utopia\Database\Exception\Authorization;
|
|||
use Utopia\Database\Exception\Structure;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Queue\Message;
|
||||
use Utopia\Queue\Result\Commit;
|
||||
use Utopia\Queue\Result\NoCommit;
|
||||
use Utopia\System\System;
|
||||
|
||||
class Audits extends Action
|
||||
{
|
||||
protected const BATCH_SIZE_DEVELOPMENT = 1; // smaller batch size for development
|
||||
protected const BATCH_SIZE_PRODUCTION = 5_000;
|
||||
protected const BATCH_AGGREGATION_INTERVAL = 60; // in seconds
|
||||
protected const int BATCH_AGGREGATION_INTERVAL = 60; // in seconds
|
||||
|
||||
private int $lastTriggeredTime = 0;
|
||||
|
||||
|
|
@ -27,9 +27,7 @@ class Audits extends Action
|
|||
|
||||
protected function getBatchSize(): int
|
||||
{
|
||||
return System::getEnv('_APP_ENV', 'development') === 'development'
|
||||
? self::BATCH_SIZE_DEVELOPMENT
|
||||
: self::BATCH_SIZE_PRODUCTION;
|
||||
return intval(System::getEnv('_APP_QUEUE_PREFETCH_COUNT', 1));
|
||||
}
|
||||
|
||||
public static function getName(): string
|
||||
|
|
@ -57,13 +55,13 @@ class Audits extends Action
|
|||
* @param Message $message
|
||||
* @param callable $getProjectDB
|
||||
* @param Document $project
|
||||
* @return void
|
||||
* @return Commit|NoCommit
|
||||
* @throws Throwable
|
||||
* @throws \Utopia\Database\Exception
|
||||
* @throws Authorization
|
||||
* @throws Structure
|
||||
*/
|
||||
public function action(Message $message, callable $getProjectDB, Document $project): void
|
||||
public function action(Message $message, callable $getProjectDB, Document $project): Commit|NoCommit
|
||||
{
|
||||
$payload = $message->getPayload() ?? [];
|
||||
|
||||
|
|
@ -123,29 +121,32 @@ class Audits extends Action
|
|||
|
||||
// Check if we should process the batch by checking both for the batch size and the elapsed time
|
||||
$batchSize = $this->getBatchSize();
|
||||
$shouldProcessBatch = \count($this->logs) >= $batchSize;
|
||||
if (!$shouldProcessBatch && \count($this->logs) > 0) {
|
||||
$logCount = array_reduce($this->logs, fn (int $current, $logs) => $current + count($logs['logs']), 0);
|
||||
$shouldProcessBatch = $logCount >= $batchSize;
|
||||
if (!$shouldProcessBatch && $logCount > 0) {
|
||||
$shouldProcessBatch = (\time() - $this->lastTriggeredTime) >= self::BATCH_AGGREGATION_INTERVAL;
|
||||
}
|
||||
|
||||
if ($shouldProcessBatch) {
|
||||
try {
|
||||
foreach ($this->logs as $internalId => $projectLogs) {
|
||||
$dbForProject = $getProjectDB($projectLogs['project']);
|
||||
|
||||
Console::log('Processing batch with ' . count($projectLogs['logs']) . ' events');
|
||||
$audit = new Audit($dbForProject);
|
||||
|
||||
$audit->logBatch($projectLogs['logs']);
|
||||
Console::success('Audit logs processed successfully');
|
||||
|
||||
unset($this->logs[$internalId]);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
Console::error('Error processing audit logs: ' . $e->getMessage());
|
||||
} finally {
|
||||
$this->lastTriggeredTime = time();
|
||||
}
|
||||
if (!$shouldProcessBatch) {
|
||||
return new NoCommit();
|
||||
}
|
||||
|
||||
try {
|
||||
foreach ($this->logs as $internalId => $projectLogs) {
|
||||
$dbForProject = $getProjectDB($projectLogs['project']);
|
||||
|
||||
Console::log('Processing batch with ' . count($projectLogs['logs']) . ' events');
|
||||
$audit = new Audit($dbForProject);
|
||||
|
||||
$audit->logBatch($projectLogs['logs']);
|
||||
Console::success('Audit logs processed successfully');
|
||||
|
||||
unset($this->logs[$internalId]);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
Console::error('Error processing audit logs: ' . $e->getMessage());
|
||||
}
|
||||
$this->lastTriggeredTime = time();
|
||||
return new Commit();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,206 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Workers;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Queue\Message;
|
||||
use Utopia\Registry\Registry;
|
||||
use Utopia\System\System;
|
||||
|
||||
/**
|
||||
* TODO remove later
|
||||
*/
|
||||
class StatsUsageDump extends Action
|
||||
{
|
||||
public const METRIC_COLLECTION_LEVEL_STORAGE = 4;
|
||||
public const METRIC_DATABASE_LEVEL_STORAGE = 3;
|
||||
public const METRIC_PROJECT_LEVEL_STORAGE = 2;
|
||||
protected array $stats = [];
|
||||
|
||||
protected Registry $register;
|
||||
|
||||
/**
|
||||
* Metrics to skip writing to logsDB
|
||||
* As these metrics are calculated separately
|
||||
* by logs DB
|
||||
* @var array
|
||||
*/
|
||||
protected array $skipBaseMetrics = [
|
||||
METRIC_DATABASES => true,
|
||||
METRIC_BUCKETS => true,
|
||||
METRIC_USERS => true,
|
||||
METRIC_FUNCTIONS => true,
|
||||
METRIC_TEAMS => true,
|
||||
METRIC_MESSAGES => true,
|
||||
METRIC_MAU => true,
|
||||
METRIC_WEBHOOKS => true,
|
||||
METRIC_PLATFORMS => true,
|
||||
METRIC_PROVIDERS => true,
|
||||
METRIC_TOPICS => true,
|
||||
METRIC_KEYS => true,
|
||||
METRIC_FILES => true,
|
||||
METRIC_FILES_STORAGE => true,
|
||||
METRIC_DEPLOYMENTS_STORAGE => true,
|
||||
METRIC_BUILDS_STORAGE => true,
|
||||
METRIC_DEPLOYMENTS => true,
|
||||
METRIC_BUILDS => true,
|
||||
METRIC_COLLECTIONS => true,
|
||||
METRIC_DOCUMENTS => true,
|
||||
METRIC_DATABASES_STORAGE => true,
|
||||
];
|
||||
|
||||
/**
|
||||
* Skip metrics associated with parent IDs
|
||||
* these need to be checked individually with `str_ends_with`
|
||||
*/
|
||||
protected array $skipParentIdMetrics = [
|
||||
'.files',
|
||||
'.files.storage',
|
||||
'.collections',
|
||||
'.documents',
|
||||
'.deployments',
|
||||
'.deployments.storage',
|
||||
'.builds',
|
||||
'.builds.storage',
|
||||
'.databases.storage'
|
||||
];
|
||||
|
||||
/**
|
||||
* @var callable(Document): Database
|
||||
*/
|
||||
protected $getLogsDB;
|
||||
|
||||
protected array $periods = [
|
||||
'1h' => 'Y-m-d H:00',
|
||||
'1d' => 'Y-m-d 00:00',
|
||||
'inf' => '0000-00-00 00:00'
|
||||
];
|
||||
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'stats-usage-dump';
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->inject('message')
|
||||
->inject('getProjectDB')
|
||||
->inject('getLogsDB')
|
||||
->inject('register')
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Message $message
|
||||
* @param callable $getProjectDB
|
||||
* @param callable $getLogsDB
|
||||
* @param Registry $register
|
||||
* @return void
|
||||
* @throws Exception
|
||||
* @throws \Throwable
|
||||
* @throws \Utopia\Database\Exception
|
||||
*/
|
||||
public function action(Message $message, callable $getProjectDB, callable $getLogsDB, Registry $register): void
|
||||
{
|
||||
$this->getLogsDB = $getLogsDB;
|
||||
$this->register = $register;
|
||||
$payload = $message->getPayload() ?? [];
|
||||
if (empty($payload)) {
|
||||
throw new Exception('Missing payload');
|
||||
}
|
||||
|
||||
foreach ($payload['stats'] ?? [] as $stats) {
|
||||
$project = new Document($stats['project'] ?? []);
|
||||
|
||||
$numberOfKeys = !empty($stats['keys']) ? count($stats['keys']) : 0;
|
||||
$receivedAt = $stats['receivedAt'] ?? null;
|
||||
if ($numberOfKeys === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Console::log('['.DateTime::now().'] Id: '.$project->getId(). ' InternalId: '.$project->getInternalId(). ' Db: '.$project->getAttribute('database').' ReceivedAt: '.$receivedAt. ' Keys: '.$numberOfKeys);
|
||||
|
||||
try {
|
||||
/** @var Database $dbForProject */
|
||||
$dbForProject = $getProjectDB($project);
|
||||
foreach ($stats['keys'] ?? [] as $key => $value) {
|
||||
if ($value == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_contains($key, METRIC_DATABASES_STORAGE)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($this->periods as $period => $format) {
|
||||
$time = null;
|
||||
|
||||
if ($period !== 'inf') {
|
||||
$time = !empty($receivedAt) ? (new \DateTime($receivedAt))->format($format) : date($format, time());
|
||||
}
|
||||
$id = \md5("{$time}_{$period}_{$key}");
|
||||
|
||||
$document = new Document([
|
||||
'$id' => $id,
|
||||
'period' => $period,
|
||||
'time' => $time,
|
||||
'metric' => $key,
|
||||
'value' => $value,
|
||||
'region' => System::getEnv('_APP_REGION', 'default'),
|
||||
]);
|
||||
|
||||
$documentClone = clone $document;
|
||||
|
||||
$dbForProject->createOrUpdateDocumentsWithIncrease(
|
||||
'stats',
|
||||
'value',
|
||||
[$document]
|
||||
);
|
||||
|
||||
$this->writeToLogsDB($project, $documentClone);
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Console::error('[' . DateTime::now() . '] project [' . $project->getInternalId() . '] database [' . $project['database'] . '] ' . ' ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function writeToLogsDB(Document $project, Document $document): void
|
||||
{
|
||||
if (System::getEnv('_APP_STATS_USAGE_DUAL_WRITING', 'disabled') === 'disabled') {
|
||||
Console::log('Dual Writing is disabled. Skipping...');
|
||||
return;
|
||||
}
|
||||
|
||||
if (array_key_exists($document->getAttribute('metric'), $this->skipBaseMetrics)) {
|
||||
return;
|
||||
}
|
||||
foreach ($this->skipParentIdMetrics as $skipMetric) {
|
||||
if (str_ends_with($document->getAttribute('metric'), $skipMetric)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$dbForLogs = ($this->getLogsDB)($project);
|
||||
|
||||
try {
|
||||
$dbForLogs->createOrUpdateDocumentsWithIncrease(
|
||||
'stats',
|
||||
'value',
|
||||
[$document]
|
||||
);
|
||||
Console::success('Usage logs pushed to Logs DB');
|
||||
} catch (\Throwable $th) {
|
||||
Console::error($th->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -113,6 +113,16 @@ abstract class Format
|
|||
protected function getEnumName(string $service, string $method, string $param): ?string
|
||||
{
|
||||
switch ($service) {
|
||||
case 'proxy':
|
||||
switch ($method) {
|
||||
case 'createRedirectRule':
|
||||
switch ($param) {
|
||||
case 'resourceType':
|
||||
return 'ProxyResourceType';
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'console':
|
||||
switch ($method) {
|
||||
case 'getResource':
|
||||
|
|
@ -441,7 +451,13 @@ abstract class Format
|
|||
case 'proxy':
|
||||
switch ($method) {
|
||||
case 'createRedirectRule':
|
||||
return ['Moved Permanently 301', 'Found 302', 'Temporary Redirect 307', 'Permanent Redirect 308'];
|
||||
switch ($param) {
|
||||
case 'statusCode':
|
||||
return ['Moved Permanently 301', 'Found 302', 'Temporary Redirect 307', 'Permanent Redirect 308'];
|
||||
case 'resourceType':
|
||||
return ['Site', 'Function'];
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'functions':
|
||||
|
|
|
|||
|
|
@ -19,21 +19,14 @@ class Executor
|
|||
public const METHOD_CONNECT = 'CONNECT';
|
||||
public const METHOD_TRACE = 'TRACE';
|
||||
|
||||
private bool $selfSigned = false;
|
||||
|
||||
/**
|
||||
* @var callable(string, string): string $endpoint
|
||||
*/
|
||||
private $endpointSelector;
|
||||
protected bool $selfSigned = false;
|
||||
|
||||
protected string $endpoint;
|
||||
protected array $headers;
|
||||
|
||||
/**
|
||||
* @param callable(string, string): string $endpointSelector
|
||||
*/
|
||||
public function __construct(callable $endpointSelector)
|
||||
public function __construct()
|
||||
{
|
||||
$this->endpointSelector = $endpointSelector;
|
||||
$this->endpoint = System::getEnv('_APP_EXECUTOR_HOST', '');
|
||||
$this->headers = [
|
||||
'content-type' => 'application/json',
|
||||
'authorization' => 'Bearer ' . System::getEnv('_APP_EXECUTOR_SECRET', ''),
|
||||
|
|
@ -97,8 +90,8 @@ class Executor
|
|||
'outputDirectory' => $outputDirectory
|
||||
];
|
||||
|
||||
$endpoint = $this->selectEndpoint($projectId, $deploymentId);
|
||||
$response = $this->call($endpoint, self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId ], $params, true, $timeout);
|
||||
|
||||
$response = $this->call($this->endpoint, self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId ], $params, true, $timeout);
|
||||
|
||||
$status = $response['headers']['status-code'];
|
||||
if ($status >= 400) {
|
||||
|
|
@ -128,8 +121,7 @@ class Executor
|
|||
'timeout' => $timeout
|
||||
];
|
||||
|
||||
$endpoint = $this->selectEndpoint($projectId, $deploymentId);
|
||||
$this->call($endpoint, self::METHOD_GET, $route, [ 'x-opr-runtime-id' => $runtimeId ], $params, true, $timeout, $callback);
|
||||
$this->call($this->endpoint, self::METHOD_GET, $route, [ 'x-opr-runtime-id' => $runtimeId ], $params, true, $timeout, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -145,8 +137,7 @@ class Executor
|
|||
$runtimeId = "$projectId-$deploymentId" . $suffix;
|
||||
$route = "/runtimes/$runtimeId";
|
||||
|
||||
$endpoint = $this->selectEndpoint($projectId, $deploymentId);
|
||||
$response = $this->call($endpoint, self::METHOD_DELETE, $route, [
|
||||
$response = $this->call($this->endpoint, self::METHOD_DELETE, $route, [
|
||||
'x-opr-addressing-method' => 'broadcast'
|
||||
], [], true, 30);
|
||||
|
||||
|
|
@ -239,8 +230,7 @@ class Executor
|
|||
$requestTimeout = $timeout + 15;
|
||||
}
|
||||
|
||||
$endpoint = $this->selectEndpoint($projectId, $deploymentId);
|
||||
$response = $this->call($endpoint, self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId, 'content-type' => 'multipart/form-data', 'accept' => 'multipart/form-data' ], $params, true, $requestTimeout);
|
||||
$response = $this->call($this->endpoint, self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId, 'content-type' => 'multipart/form-data', 'accept' => 'multipart/form-data' ], $params, true, $requestTimeout);
|
||||
|
||||
$status = $response['headers']['status-code'];
|
||||
if ($status >= 400) {
|
||||
|
|
@ -274,8 +264,7 @@ class Executor
|
|||
'timeout' => $timeout
|
||||
];
|
||||
|
||||
$endpoint = $this->selectEndpoint($projectId, $deploymentId);
|
||||
$response = $this->call($endpoint, self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId ], $params, true, $timeout);
|
||||
$response = $this->call($this->endpoint, self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId ], $params, true, $timeout);
|
||||
|
||||
$status = $response['headers']['status-code'];
|
||||
if ($status >= 400) {
|
||||
|
|
@ -465,9 +454,4 @@ class Executor
|
|||
|
||||
return $output;
|
||||
}
|
||||
|
||||
private function selectEndpoint(string $projectId, string $deploymentId): string
|
||||
{
|
||||
return call_user_func($this->endpointSelector, $projectId, $deploymentId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ trait ProxyBase
|
|||
return $rule;
|
||||
}
|
||||
|
||||
protected function createRedirectRule(string $domain, string $url, int $statusCode): mixed
|
||||
protected function createRedirectRule(string $domain, string $url, int $statusCode, string $resourceType, string $resourceId): mixed
|
||||
{
|
||||
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/redirect', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
|
|
@ -77,6 +77,8 @@ trait ProxyBase
|
|||
'domain' => $domain,
|
||||
'url' => $url,
|
||||
'statusCode' => $statusCode,
|
||||
'resourceType' => $resourceType,
|
||||
'resourceId' => $resourceId,
|
||||
]);
|
||||
|
||||
return $rule;
|
||||
|
|
@ -115,9 +117,9 @@ trait ProxyBase
|
|||
return $rule['body']['$id'];
|
||||
}
|
||||
|
||||
protected function setupRedirectRule(string $domain, string $url, int $statusCode): string
|
||||
protected function setupRedirectRule(string $domain, string $url, int $statusCode, string $resourceType, string $resourceId): string
|
||||
{
|
||||
$rule = $this->createRedirectRule($domain, $url, $statusCode);
|
||||
$rule = $this->createRedirectRule($domain, $url, $statusCode, $resourceType, $resourceId);
|
||||
|
||||
$this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule));
|
||||
|
||||
|
|
|
|||
|
|
@ -131,7 +131,9 @@ class ProxyCustomServerTest extends Scope
|
|||
$response = $proxyClient->call(Client::METHOD_GET, '/todos/1');
|
||||
$this->assertEquals(404, $response['headers']['status-code']);
|
||||
|
||||
$ruleId = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 301);
|
||||
$siteId = $this->setupSite()['siteId'];
|
||||
|
||||
$ruleId = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 301, 'site', $siteId);
|
||||
$this->assertNotEmpty($ruleId);
|
||||
|
||||
$response = $proxyClient->call(Client::METHOD_GET, '/todos/1');
|
||||
|
|
@ -147,7 +149,7 @@ class ProxyCustomServerTest extends Scope
|
|||
$this->assertEquals('https://jsonplaceholder.typicode.com/todos/1', $response['headers']['location']);
|
||||
|
||||
$domain = \uniqid() . '-redirect-307.custom.localhost';
|
||||
$ruleId = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 307);
|
||||
$ruleId = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 307, 'site', $siteId);
|
||||
$this->assertNotEmpty($ruleId);
|
||||
|
||||
$proxyClient = new Client();
|
||||
|
|
@ -158,6 +160,18 @@ class ProxyCustomServerTest extends Scope
|
|||
$this->assertEquals(307, $response['headers']['status-code']);
|
||||
$this->assertEquals('https://jsonplaceholder.typicode.com/todos/1', $response['headers']['location']);
|
||||
|
||||
$rules = $this->listRules([
|
||||
'queries' => [
|
||||
Query::equal('type', ['redirect'])->toString(),
|
||||
Query::equal('trigger', ['manual'])->toString(),
|
||||
Query::equal('deploymentResourceType', ['site'])->toString(),
|
||||
Query::equal('deploymentResourceId', [$siteId])->toString(),
|
||||
],
|
||||
]);
|
||||
$this->assertEquals(200, $rules['headers']['status-code']);
|
||||
$this->assertEquals(2, $rules['body']['total']);
|
||||
|
||||
$this->cleanupSite($siteId);
|
||||
$this->cleanupRule($ruleId);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue