mirror of
https://github.com/appwrite/appwrite
synced 2026-05-23 08:58:35 +00:00
Merge branch 'feat-usage-updates' of https://github.com/appwrite/appwrite into feat-refactor-usage-stats
This commit is contained in:
commit
178414cef4
24 changed files with 978 additions and 30 deletions
2
.env
2
.env
|
|
@ -75,7 +75,7 @@ _APP_MAINTENANCE_INTERVAL=86400
|
|||
_APP_MAINTENANCE_RETENTION_EXECUTION=1209600
|
||||
_APP_MAINTENANCE_RETENTION_ABUSE=86400
|
||||
_APP_MAINTENANCE_RETENTION_AUDIT=1209600
|
||||
_APP_USAGE_AGGREGATION_INTERVAL=30
|
||||
_APP_USAGE_AGGREGATION_INTERVAL=15
|
||||
_APP_USAGE_STATS=enabled
|
||||
_APP_LOGGING_PROVIDER=
|
||||
_APP_LOGGING_CONFIG=
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -184,7 +184,7 @@ App::get('/v1/functions/:functionId/usage')
|
|||
->label('scope', 'functions.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'functions')
|
||||
->label('sdk.method', 'getUsage')
|
||||
->label('sdk.method', 'getFunctionUsage')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_USAGE_FUNCTIONS)
|
||||
|
|
@ -273,6 +273,101 @@ App::get('/v1/functions/:functionId/usage')
|
|||
]);
|
||||
}
|
||||
|
||||
$response->dynamic($usage, Response::MODEL_USAGE_FUNCTION);
|
||||
});
|
||||
|
||||
App::get('/v1/functions/usage')
|
||||
->desc('Get Functions Usage')
|
||||
->groups(['api', 'functions'])
|
||||
->label('scope', 'functions.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'functions')
|
||||
->label('sdk.method', 'getUsage')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_USAGE_FUNCTIONS)
|
||||
->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d']), 'Date range.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (string $range, Response $response, Database $dbForProject) {
|
||||
|
||||
$usage = [];
|
||||
if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') {
|
||||
$periods = [
|
||||
'24h' => [
|
||||
'period' => '30m',
|
||||
'limit' => 48,
|
||||
],
|
||||
'7d' => [
|
||||
'period' => '1d',
|
||||
'limit' => 7,
|
||||
],
|
||||
'30d' => [
|
||||
'period' => '1d',
|
||||
'limit' => 30,
|
||||
],
|
||||
'90d' => [
|
||||
'period' => '1d',
|
||||
'limit' => 90,
|
||||
],
|
||||
];
|
||||
|
||||
$metrics = [
|
||||
"functions.executions",
|
||||
"functions.failures",
|
||||
"functions.executionTime",
|
||||
"functions.buildTime",
|
||||
"functions.compute"
|
||||
];
|
||||
|
||||
$stats = [];
|
||||
|
||||
Authorization::skip(function () use ($dbForProject, $periods, $range, $metrics, &$stats) {
|
||||
foreach ($metrics as $metric) {
|
||||
$limit = $periods[$range]['limit'];
|
||||
$period = $periods[$range]['period'];
|
||||
|
||||
$requestDocs = $dbForProject->find('stats', [
|
||||
new Query('period', Query::TYPE_EQUAL, [$period]),
|
||||
new Query('metric', Query::TYPE_EQUAL, [$metric]),
|
||||
], $limit, 0, ['time'], [Database::ORDER_DESC]);
|
||||
|
||||
$stats[$metric] = [];
|
||||
foreach ($requestDocs as $requestDoc) {
|
||||
$stats[$metric][] = [
|
||||
'value' => $requestDoc->getAttribute('value'),
|
||||
'date' => $requestDoc->getAttribute('time'),
|
||||
];
|
||||
}
|
||||
|
||||
// backfill metrics with empty values for graphs
|
||||
$backfill = $limit - \count($requestDocs);
|
||||
while ($backfill > 0) {
|
||||
$last = $limit - $backfill - 1; // array index of last added metric
|
||||
$diff = match ($period) { // convert period to seconds for unix timestamp math
|
||||
'30m' => 1800,
|
||||
'1d' => 86400,
|
||||
};
|
||||
$stats[$metric][] = [
|
||||
'value' => 0,
|
||||
'date' => ($stats[$metric][$last]['date'] ?? \time()) - $diff, // time of last metric minus period
|
||||
];
|
||||
$backfill--;
|
||||
}
|
||||
$stats[$metric] = array_reverse($stats[$metric]);
|
||||
}
|
||||
});
|
||||
|
||||
$usage = new Document([
|
||||
'range' => $range,
|
||||
'functionsExecutions' => $stats["functions.executions"],
|
||||
'functionsFailures' => $stats["functions.failures"],
|
||||
'functionsCompute' => $stats["functions.compute"],
|
||||
'functionsExecutionTime' => $stats["functions.executionTime"],
|
||||
'functionsBuildTime' => $stats["functions.buildTime"]
|
||||
]);
|
||||
}
|
||||
|
||||
$response->dynamic($usage, Response::MODEL_USAGE_FUNCTIONS);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ $cli
|
|||
*/
|
||||
$usage->collect();
|
||||
|
||||
if ($iterations % 30 != 0) { // return if 30 iterations has not passed
|
||||
if ($iterations % 30 != 0 && App::getEnv('_APP_ENV', 'production') == 'production') { // return if 30 iterations has not passed
|
||||
$iterations++;
|
||||
$loopTook = microtime(true) - $loopStart;
|
||||
$now = date('d-m-Y H:i:s', time());
|
||||
|
|
|
|||
|
|
@ -291,7 +291,7 @@ sort($patterns);
|
|||
<li data-state="/console/functions/function/monitors?id={{router.params.id}}&project={{router.params.project}}">
|
||||
|
||||
<form class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '90d'"
|
||||
data-service="functions.getUsage"
|
||||
data-service="functions.getFunctionUsage"
|
||||
data-event="submit"
|
||||
data-name="usage"
|
||||
data-param-function-id="{{router.params.id}}"
|
||||
|
|
@ -302,9 +302,10 @@ sort($patterns);
|
|||
<button class="tick pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} === '90d'" disabled>90d</button>
|
||||
|
||||
<form class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '30d'"
|
||||
data-service="functions.getUsage"
|
||||
data-service="functions.getFunctionUsage"
|
||||
data-event="submit"
|
||||
data-name="usage"
|
||||
data-param-range="30d"
|
||||
data-param-function-id="{{router.params.id}}">
|
||||
<button class="tick">30d</button>
|
||||
</form>
|
||||
|
|
@ -312,7 +313,7 @@ sort($patterns);
|
|||
<button class="tick pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} === '30d'" disabled>30d</button>
|
||||
|
||||
<form class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '24h'"
|
||||
data-service="functions.getUsage"
|
||||
data-service="functions.getFunctionUsage"
|
||||
data-event="submit"
|
||||
data-name="usage"
|
||||
data-param-function-id="{{router.params.id}}"
|
||||
|
|
@ -325,7 +326,7 @@ sort($patterns);
|
|||
<h2>Monitors</h2>
|
||||
|
||||
<div
|
||||
data-service="functions.getUsage"
|
||||
data-service="functions.getFunctionUsage"
|
||||
data-event="load"
|
||||
data-name="usage"
|
||||
data-param-function-id="{{router.params.id}}">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
$runtimes = $this->getParam('runtimes', []);
|
||||
$usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
|
||||
?>
|
||||
<div class="cover">
|
||||
<h1 class="zone xl margin-bottom-large">
|
||||
|
|
@ -136,5 +137,82 @@ $runtimes = $this->getParam('runtimes', []);
|
|||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<?php if ($usageStatsEnabled): ?>
|
||||
<li data-state="/console/functions/usage?id={{router.params.id}}&project={{router.params.project}}">
|
||||
|
||||
<form class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '90d'"
|
||||
data-service="functions.getUsage"
|
||||
data-event="submit"
|
||||
data-name="usage"
|
||||
data-param-range="90d">
|
||||
<button class="tick">90d</button>
|
||||
</form>
|
||||
|
||||
<button class="tick pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} === '90d'" disabled>90d</button>
|
||||
|
||||
<form class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '30d'"
|
||||
data-service="functions.getUsage"
|
||||
data-event="submit"
|
||||
data-name="usage"
|
||||
data-param-range="30d">
|
||||
<button class="tick">30d</button>
|
||||
</form>
|
||||
|
||||
<button class="tick pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} === '30d'" disabled>30d</button>
|
||||
|
||||
<form class="pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} !== '24h'"
|
||||
data-service="functions.getUsage"
|
||||
data-event="submit"
|
||||
data-name="usage"
|
||||
data-param-range="24h">
|
||||
<button class="tick">24h</button>
|
||||
</form>
|
||||
|
||||
<button class="tick pull-end margin-start-small margin-top-small" data-ls-if="{{usage.range}} === '24h'" disabled>24h</button>
|
||||
|
||||
<h2>Usage</h2>
|
||||
|
||||
<div
|
||||
data-service="functions.getUsage"
|
||||
data-event="load"
|
||||
data-name="usage">
|
||||
<div class="box margin-bottom-small">
|
||||
<div class="margin-start-negative-small margin-end-negative-small margin-top-negative-small margin-bottom-negative-small">
|
||||
<div class="chart background-image-no border-no margin-bottom-no">
|
||||
<input type="hidden" data-ls-bind="{{usage}}" data-forms-chart="Executions=functionsExecutions" data-height="140" data-show-y-axis="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="chart-notes margin-bottom-large">
|
||||
<li>Executions <span data-ls-bind="({{usage.functionsExecutions|statsGetLast|statsTotal}})"></span></li>
|
||||
</ul>
|
||||
|
||||
<div class="box margin-bottom-small">
|
||||
<div class="margin-start-negative-small margin-end-negative-small margin-top-negative-small margin-bottom-negative-small">
|
||||
<div class="chart background-image-no border-no margin-bottom-no">
|
||||
<input type="hidden" data-ls-bind="{{usage}}" data-forms-chart="CPU Time (milliseconds)=functionsCompute" data-colors="orange" data-height="140" data-show-y-axis="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="chart-notes margin-bottom-large">
|
||||
<li class="orange">CPU Time <span data-ls-bind="({{usage.functionsCompute|statsGetLast|seconds2hum}})"></span></li>
|
||||
</ul>
|
||||
|
||||
<div class="box margin-bottom-small">
|
||||
<div class="margin-start-negative-small margin-end-negative-small margin-top-negative-small margin-bottom-negative-small">
|
||||
<div class="chart background-image-no border-no margin-bottom-no">
|
||||
<input type="hidden" data-ls-bind="{{usage}}" data-forms-chart="Failures=functionsFailures" data-colors="red" data-height="140" data-show-y-axis="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="chart-notes margin-bottom-large">
|
||||
<li class="red">Errors <span data-ls-bind="({{usage.functionsFailures|statsGetLast|statsTotal}})"></span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<?php endif;?>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -6,7 +6,7 @@ use Appwrite\Resque\Worker;
|
|||
use Appwrite\Utopia\Response\Model\Deployment;
|
||||
use Cron\CronExpression;
|
||||
use Executor\Executor;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Appwrite\Stats\Stats;
|
||||
use Utopia\App;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Storage\Storage;
|
||||
|
|
@ -213,6 +213,22 @@ class BuildsV1 extends Worker
|
|||
channels: $target['channels'],
|
||||
roles: $target['roles']
|
||||
);
|
||||
|
||||
/** Update usage stats */
|
||||
global $register;
|
||||
if (App::getEnv('_APP_USAGE_STATS', 'enabled') === 'enabled') {
|
||||
$statsd = $register->get('statsd');
|
||||
$usage = new Stats($statsd);
|
||||
$usage
|
||||
->setParam('projectId', $project->getId())
|
||||
->setParam('functionId', $function->getId())
|
||||
->setParam('functionBuild', 1)
|
||||
->setParam('functionBuildStatus', $build->getAttribute('status', ''))
|
||||
->setParam('functionBuildTime', $build->getAttribute('duration') * 1000) // ms
|
||||
->setParam('networkRequestSize', 0)
|
||||
->setParam('networkResponseSize', 0)
|
||||
->submit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
5
public/dist/scripts/app-all.js
vendored
5
public/dist/scripts/app-all.js
vendored
File diff suppressed because one or more lines are too long
3
public/dist/scripts/app-dep.js
vendored
3
public/dist/scripts/app-dep.js
vendored
|
|
@ -382,8 +382,9 @@ let path='/functions/{functionId}/executions'.replace('{functionId}',functionId)
|
|||
if(typeof async!=='undefined'){payload['async']=async;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),getExecution:(functionId,executionId)=>__awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');}
|
||||
if(typeof executionId==='undefined'){throw new AppwriteException('Missing required parameter: "executionId"');}
|
||||
let path='/functions/{functionId}/executions/{executionId}'.replace('{functionId}',functionId).replace('{executionId}',executionId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getUsage:(functionId,range)=>__awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');}
|
||||
let path='/functions/{functionId}/executions/{executionId}'.replace('{functionId}',functionId).replace('{executionId}',executionId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getFunctionUsage:(functionId,range)=>__awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');}
|
||||
let path='/functions/{functionId}/usage'.replace('{functionId}',functionId);let payload={};if(typeof range!=='undefined'){payload['range']=range;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getUsage:(range)=>__awaiter(this,void 0,void 0,function*(){let path='/functions/usage';let payload={};if(typeof range!=='undefined'){payload['range']=range;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);})};this.health={get:()=>__awaiter(this,void 0,void 0,function*(){let path='/health';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getAntivirus:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/anti-virus';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getCache:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/cache';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getDB:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/db';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getQueueCertificates:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/queue/certificates';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getQueueFunctions:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/queue/functions';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getQueueLogs:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/queue/logs';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getQueueWebhooks:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/queue/webhooks';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getStorageLocal:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/storage/local';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getTime:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/time';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);})};this.locale={get:()=>__awaiter(this,void 0,void 0,function*(){let path='/locale';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getContinents:()=>__awaiter(this,void 0,void 0,function*(){let path='/locale/continents';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getCountries:()=>__awaiter(this,void 0,void 0,function*(){let path='/locale/countries';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getCountriesEU:()=>__awaiter(this,void 0,void 0,function*(){let path='/locale/countries/eu';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getCountriesPhones:()=>__awaiter(this,void 0,void 0,function*(){let path='/locale/countries/phones';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getCurrencies:()=>__awaiter(this,void 0,void 0,function*(){let path='/locale/currencies';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getLanguages:()=>__awaiter(this,void 0,void 0,function*(){let path='/locale/languages';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);})};this.projects={list:(search,limit,offset,cursor,cursorDirection,orderType)=>__awaiter(this,void 0,void 0,function*(){let path='/projects';let payload={};if(typeof search!=='undefined'){payload['search']=search;}
|
||||
if(typeof limit!=='undefined'){payload['limit']=limit;}
|
||||
if(typeof offset!=='undefined'){payload['offset']=offset;}
|
||||
|
|
|
|||
2
public/dist/scripts/app.js
vendored
2
public/dist/scripts/app.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -3194,7 +3194,7 @@
|
|||
* @throws {AppwriteException}
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getUsage: (functionId, range) => __awaiter(this, void 0, void 0, function* () {
|
||||
getFunctionUsage: (functionId, range) => __awaiter(this, void 0, void 0, function* () {
|
||||
if (typeof functionId === 'undefined') {
|
||||
throw new AppwriteException('Missing required parameter: "functionId"');
|
||||
}
|
||||
|
|
@ -3207,6 +3207,25 @@
|
|||
return yield this.call('get', uri, {
|
||||
'content-type': 'application/json',
|
||||
}, payload);
|
||||
}),
|
||||
/**
|
||||
* Get Function Usage
|
||||
*
|
||||
*
|
||||
* @param {string} range
|
||||
* @throws {AppwriteException}
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getUsage: (range) => __awaiter(this, void 0, void 0, function* () {
|
||||
let path = '/functions/usage';
|
||||
let payload = {};
|
||||
if (typeof range !== 'undefined') {
|
||||
payload['range'] = range;
|
||||
}
|
||||
const uri = new URL(this.config.endpoint + path);
|
||||
return yield this.call('get', uri, {
|
||||
'content-type': 'application/json',
|
||||
}, payload);
|
||||
})
|
||||
};
|
||||
this.health = {
|
||||
|
|
|
|||
|
|
@ -232,6 +232,13 @@ window.ls.router
|
|||
scope: "console",
|
||||
project: true
|
||||
})
|
||||
.add("/console/functions/usage", {
|
||||
template: function(window) {
|
||||
return window.location.pathname + window.location.search + '&version=' + APP_ENV.CACHEBUSTER;
|
||||
},
|
||||
scope: "console",
|
||||
project: true
|
||||
})
|
||||
.add("/console/functions/function", {
|
||||
template: "/console/functions/function?version=" + APP_ENV.CACHEBUSTER,
|
||||
scope: "console",
|
||||
|
|
|
|||
|
|
@ -93,6 +93,10 @@ class Stats
|
|||
$functionExecution = $this->params['functionExecution'] ?? 0;
|
||||
$functionExecutionTime = $this->params['functionExecutionTime'] ?? 0;
|
||||
$functionStatus = $this->params['functionStatus'] ?? '';
|
||||
$functionBuildTime = $this->params['functionBuildTime'] ?? 0;
|
||||
$functionBuild = $this->params['functionBuild'] ?? 0;
|
||||
$functionBuildStatus = $this->params['functionBuildStatus'] ?? '';
|
||||
$functionCompute = $functionExecutionTime + $functionBuildTime;
|
||||
|
||||
$tags = ",projectId={$projectId},version=" . App::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
|
||||
|
|
@ -104,8 +108,15 @@ class Stats
|
|||
}
|
||||
|
||||
if ($functionExecution >= 1) {
|
||||
$this->statsd->increment('executions.all' . $tags . ',functionId=' . $functionId . ',functionStatus=' . $functionStatus);
|
||||
$this->statsd->count('executions.time' . $tags . ',functionId=' . $functionId, $functionExecutionTime);
|
||||
$this->statsd->increment('functions.executions.all' . $tags . ',functionId=' . $functionId . ',functionStatus=' . $functionStatus);
|
||||
$this->statsd->count('functions.executions.time' . $tags . ',functionId=' . $functionId, $functionExecutionTime);
|
||||
}
|
||||
if ($functionBuild >= 1) {
|
||||
$this->statsd->increment('functions.builds.all' . $tags . ',functionId=' . $functionId . ',functionBuildStatus=' . $functionBuildStatus);
|
||||
$this->statsd->count('functions.builds.time' . $tags . ',functionId=' . $functionId, $functionExecutionTime);
|
||||
}
|
||||
if ($functionBuild + $functionExecution >= 1) {
|
||||
$this->statsd->count('functions.compute.time' . $tags . ',functionId=' . $functionId, $functionCompute);
|
||||
}
|
||||
|
||||
$this->statsd->count('network.inbound' . $tags, $networkRequestSize);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,12 @@ class Usage
|
|||
'network' => [
|
||||
'table' => 'appwrite_usage_network_all',
|
||||
],
|
||||
'inbound' => [
|
||||
'table' => 'appwrite_usage_network_inbound',
|
||||
],
|
||||
'outbound' => [
|
||||
'table' => 'appwrite_usage_network_outbound',
|
||||
],
|
||||
'executions' => [
|
||||
'table' => 'appwrite_usage_executions_all',
|
||||
],
|
||||
|
|
@ -171,21 +177,52 @@ class Usage
|
|||
'users.sessions.delete' => [
|
||||
'table' => 'appwrite_usage_users_sessions_delete',
|
||||
],
|
||||
'functions.executions' => [
|
||||
'table' => 'appwrite_usage_functions_executions_all',
|
||||
],
|
||||
'functions.builds' => [
|
||||
'table' => 'appwrite_usage_functions_builds_all',
|
||||
],
|
||||
'functions.failures' => [
|
||||
'table' => 'appwrite_usage_functions_executions_all',
|
||||
'filters' => [
|
||||
'functionStatus' => 'failed',
|
||||
],
|
||||
],
|
||||
'functions.functionId.executions' => [
|
||||
'table' => 'appwrite_usage_executions_all',
|
||||
'table' => 'appwrite_usage_functions_executions_all',
|
||||
'groupBy' => ['functionId'],
|
||||
],
|
||||
'functions.functionId.compute' => [
|
||||
'table' => 'appwrite_usage_executions_time',
|
||||
'functions.functionId.builds' => [
|
||||
'table' => 'appwrite_usage_functions_builds_all',
|
||||
'groupBy' => ['functionId'],
|
||||
],
|
||||
'functions.functionId.failures' => [
|
||||
'table' => 'appwrite_usage_executions_all',
|
||||
'functions.functionId.execution' => [
|
||||
'table' => 'appwrite_usage_functions_executions_time',
|
||||
'groupBy' => ['functionId'],
|
||||
],
|
||||
'functions.functionId.build' => [
|
||||
'table' => 'appwrite_usage_functions_builds_time',
|
||||
'groupBy' => ['functionId'],
|
||||
],
|
||||
'functions.functionId.compute' => [ // Built time + execution time
|
||||
'table' => 'appwrite_usage_functions_compute_time',
|
||||
'groupBy' => ['functionId'],
|
||||
],
|
||||
'functions.functionId.executions.failures' => [
|
||||
'table' => 'appwrite_usage_functions_executions_all',
|
||||
'groupBy' => ['functionId'],
|
||||
'filters' => [
|
||||
'functionStatus' => 'failed',
|
||||
],
|
||||
],
|
||||
'functions.functionId.builds.failures' => [
|
||||
'table' => 'appwrite_usage_functions_builds_all',
|
||||
'groupBy' => ['functionId'],
|
||||
'filters' => [
|
||||
'functionBuildStatus' => 'failed',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
protected array $period = [
|
||||
|
|
@ -292,7 +329,6 @@ class Usage
|
|||
|
||||
if (!empty($projectId) && $projectId !== 'console') {
|
||||
$metricUpdated = $metric;
|
||||
|
||||
if (!empty($groupBy)) {
|
||||
foreach ($options['groupBy'] as $groupBy) {
|
||||
$groupedBy = $point[$groupBy] ?? '';
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ use Appwrite\Utopia\Response\Model\UsageBuckets;
|
|||
use Appwrite\Utopia\Response\Model\UsageCollection;
|
||||
use Appwrite\Utopia\Response\Model\UsageDatabase;
|
||||
use Appwrite\Utopia\Response\Model\UsageDatabases;
|
||||
use Appwrite\Utopia\Response\Model\UsageFunction;
|
||||
use Appwrite\Utopia\Response\Model\UsageFunctions;
|
||||
use Appwrite\Utopia\Response\Model\UsageProject;
|
||||
use Appwrite\Utopia\Response\Model\UsageStorage;
|
||||
|
|
@ -93,6 +94,7 @@ class Response extends SwooleResponse
|
|||
public const MODEL_USAGE_BUCKETS = 'usageBuckets';
|
||||
public const MODEL_USAGE_STORAGE = 'usageStorage';
|
||||
public const MODEL_USAGE_FUNCTIONS = 'usageFunctions';
|
||||
public const MODEL_USAGE_FUNCTION = 'usageFunction';
|
||||
public const MODEL_USAGE_PROJECT = 'usageProject';
|
||||
|
||||
// Database
|
||||
|
|
@ -296,6 +298,7 @@ class Response extends SwooleResponse
|
|||
->setModel(new UsageStorage())
|
||||
->setModel(new UsageBuckets())
|
||||
->setModel(new UsageFunctions())
|
||||
->setModel(new UsageFunction())
|
||||
->setModel(new UsageProject())
|
||||
// Verification
|
||||
// Recovery
|
||||
|
|
|
|||
62
src/Appwrite/Utopia/Response/Model/UsageFunction.php
Normal file
62
src/Appwrite/Utopia/Response/Model/UsageFunction.php
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class UsageFunction extends Model
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->addRule('range', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'The time range of the usage stats.',
|
||||
'default' => '',
|
||||
'example' => '30d',
|
||||
])
|
||||
->addRule('functionsExecutions', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'description' => 'Aggregated stats for function executions.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'array' => true
|
||||
])
|
||||
->addRule('functionsFailures', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'description' => 'Aggregated stats for function execution failures.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'array' => true
|
||||
])
|
||||
->addRule('functionsCompute', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'description' => 'Aggregated stats for function execution duration.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'array' => true
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'UsageFunction';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_USAGE_FUNCTION;
|
||||
}
|
||||
}
|
||||
|
|
@ -37,6 +37,20 @@ class UsageFunctions extends Model
|
|||
'example' => new \stdClass(),
|
||||
'array' => true
|
||||
])
|
||||
->addRule('functionsExecutionTime', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'description' => 'Aggregated stats for function execution duration.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'array' => true
|
||||
])
|
||||
->addRule('functionsBuildTime', [
|
||||
'type' => Response::MODEL_METRIC_LIST,
|
||||
'description' => 'Aggregated stats for function execution duration.',
|
||||
'default' => [],
|
||||
'example' => new \stdClass(),
|
||||
'array' => true
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
|
|
|
|||
599
tests/e2e/General/UsageTest.php
Normal file
599
tests/e2e/General/UsageTest.php
Normal file
|
|
@ -0,0 +1,599 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\E2E\General;
|
||||
|
||||
use Tests\E2E\Client;
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\SideServer;
|
||||
use CURLFile;
|
||||
use Tests\E2E\Services\Functions\FunctionsBase;
|
||||
|
||||
class UsageTest extends Scope
|
||||
{
|
||||
use ProjectCustom;
|
||||
use SideServer;
|
||||
use FunctionsBase;
|
||||
|
||||
protected array $headers = [];
|
||||
protected string $projectId;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testUsersStats(): array
|
||||
{
|
||||
$project = $this->getProject(true);
|
||||
$projectId = $project['$id'];
|
||||
$headers['x-appwrite-project'] = $project['$id'];
|
||||
$headers['x-appwrite-key'] = $project['apiKey'];
|
||||
$headers['content-type'] = 'application/json';
|
||||
|
||||
$usersCount = 0;
|
||||
$requestsCount = 0;
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$email = uniqid() . 'user@usage.test';
|
||||
$password = 'password';
|
||||
$name = uniqid() . 'User';
|
||||
$res = $this->client->call(Client::METHOD_POST, '/users', $headers, [
|
||||
'userId' => 'unique()',
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
'name' => $name,
|
||||
]);
|
||||
$this->assertEquals($email, $res['body']['email']);
|
||||
$this->assertNotEmpty($res['body']['$id']);
|
||||
$usersCount++;
|
||||
$requestsCount++;
|
||||
|
||||
if ($i < 5) {
|
||||
$userId = $res['body']['$id'];
|
||||
$res = $this->client->call(Client::METHOD_GET, '/users/' . $userId, $headers);
|
||||
$this->assertEquals($userId, $res['body']['$id']);
|
||||
$res = $this->client->call(Client::METHOD_DELETE, '/users/' . $userId, $headers);
|
||||
$this->assertEmpty($res['body']);
|
||||
$requestsCount += 2;
|
||||
$usersCount--;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sleep(35);
|
||||
|
||||
// console request
|
||||
$cheaders = [
|
||||
'origin' => 'http://localhost',
|
||||
'x-appwrite-project' => 'console',
|
||||
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
|
||||
];
|
||||
|
||||
$res = $this->client->call(Client::METHOD_GET, '/projects/' . $projectId . '/usage?range=30d', $cheaders);
|
||||
$res = $res['body'];
|
||||
|
||||
$this->assertEquals(8, count($res));
|
||||
$this->assertEquals(30, count($res['requests']));
|
||||
$this->assertEquals(30, count($res['users']));
|
||||
$this->assertEquals($usersCount, $res['users'][array_key_last($res['users'])]['value']);
|
||||
$this->assertEquals($requestsCount, $res['requests'][array_key_last($res['requests'])]['value']);
|
||||
|
||||
$res = $this->client->call(Client::METHOD_GET, '/users/usage?range=30d', array_merge($cheaders, [
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-mode' => 'admin'
|
||||
]));
|
||||
$res = $res['body'];
|
||||
$this->assertEquals(10, $res['usersCreate'][array_key_last($res['usersCreate'])]['value']);
|
||||
$this->assertEquals(5, $res['usersRead'][array_key_last($res['usersRead'])]['value']);
|
||||
$this->assertEquals(5, $res['usersDelete'][array_key_last($res['usersDelete'])]['value']);
|
||||
|
||||
return ['projectId' => $projectId, 'headers' => $headers, 'requestsCount' => $requestsCount];
|
||||
}
|
||||
|
||||
/** @depends testUsersStats */
|
||||
public function testStorageStats(array $data): array
|
||||
{
|
||||
$projectId = $data['projectId'];
|
||||
$headers = $data['headers'];
|
||||
|
||||
$bucketId = '';
|
||||
$bucketsCount = 0;
|
||||
$requestsCount = $data['requestsCount'];
|
||||
$storageTotal = 0;
|
||||
$bucketsCreate = 0;
|
||||
$bucketsDelete = 0;
|
||||
$bucketsRead = 0;
|
||||
$filesCount = 0;
|
||||
$filesRead = 0;
|
||||
$filesCreate = 0;
|
||||
$filesDelete = 0;
|
||||
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$name = uniqid() . ' bucket';
|
||||
$res = $this->client->call(Client::METHOD_POST, '/storage/buckets', $headers, [
|
||||
'bucketId' => 'unique()',
|
||||
'name' => $name,
|
||||
'permission' => 'bucket'
|
||||
]);
|
||||
$this->assertEquals($name, $res['body']['name']);
|
||||
$this->assertNotEmpty($res['body']['$id']);
|
||||
$bucketId = $res['body']['$id'];
|
||||
|
||||
$bucketsCreate++;
|
||||
$bucketsCount++;
|
||||
$requestsCount++;
|
||||
|
||||
if ($i < 5) {
|
||||
$res = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId, $headers);
|
||||
$this->assertEquals($bucketId, $res['body']['$id']);
|
||||
$bucketsRead++;
|
||||
|
||||
$res = $this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $bucketId, $headers);
|
||||
$this->assertEmpty($res['body']);
|
||||
$bucketsDelete++;
|
||||
|
||||
$requestsCount += 2;
|
||||
$bucketsCount--;
|
||||
}
|
||||
}
|
||||
|
||||
// upload some files
|
||||
$files = [
|
||||
[
|
||||
'path' => realpath(__DIR__ . '/../../resources/logo.png'),
|
||||
'name' => 'logo.png',
|
||||
],
|
||||
[
|
||||
'path' => realpath(__DIR__ . '/../../resources/file.png'),
|
||||
'name' => 'file.png',
|
||||
],
|
||||
[
|
||||
'path' => realpath(__DIR__ . '/../../resources/disk-a/kitten-3.gif'),
|
||||
'name' => 'kitten-3.gif',
|
||||
],
|
||||
[
|
||||
'path' => realpath(__DIR__ . '/../../resources/disk-a/kitten-1.jpg'),
|
||||
'name' => 'kitten-1.jpg',
|
||||
],
|
||||
];
|
||||
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$file = $files[$i % count($files)];
|
||||
$res = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge($headers, ['content-type' => 'multipart/form-data']), [
|
||||
'fileId' => 'unique()',
|
||||
'file' => new CURLFile($file['path'], '', $file['name']),
|
||||
]);
|
||||
$this->assertNotEmpty($res['body']['$id']);
|
||||
|
||||
$fileSize = $res['body']['sizeOriginal'];
|
||||
$storageTotal += $fileSize;
|
||||
$filesCount++;
|
||||
$filesCreate++;
|
||||
$requestsCount++;
|
||||
|
||||
$fileId = $res['body']['$id'];
|
||||
if ($i < 5) {
|
||||
$res = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $fileId, $headers);
|
||||
$this->assertEquals($fileId, $res['body']['$id']);
|
||||
$filesRead++;
|
||||
|
||||
$res = $this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $bucketId . '/files/' . $fileId, $headers);
|
||||
$this->assertEmpty($res['body']);
|
||||
$filesDelete++;
|
||||
$requestsCount += 2;
|
||||
$filesCount--;
|
||||
$storageTotal -= $fileSize;
|
||||
}
|
||||
}
|
||||
|
||||
sleep(35);
|
||||
|
||||
// console request
|
||||
$headers = [
|
||||
'origin' => 'http://localhost',
|
||||
'x-appwrite-project' => 'console',
|
||||
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
|
||||
];
|
||||
|
||||
$res = $this->client->call(Client::METHOD_GET, '/projects/' . $projectId . '/usage?range=30d', $headers);
|
||||
$res = $res['body'];
|
||||
|
||||
$this->assertEquals(8, count($res));
|
||||
$this->assertEquals(30, count($res['requests']));
|
||||
$this->assertEquals(30, count($res['storage']));
|
||||
$this->assertEquals($requestsCount, $res['requests'][array_key_last($res['requests'])]['value']);
|
||||
$this->assertEquals($storageTotal, $res['storage'][array_key_last($res['storage'])]['value']);
|
||||
|
||||
$res = $this->client->call(Client::METHOD_GET, '/storage/usage?range=30d', array_merge($headers, [
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-mode' => 'admin'
|
||||
]));
|
||||
$res = $res['body'];
|
||||
$this->assertEquals($storageTotal, $res['filesStorage'][array_key_last($res['filesStorage'])]['value']);
|
||||
$this->assertEquals($bucketsCount, $res['bucketsCount'][array_key_last($res['bucketsCount'])]['value']);
|
||||
$this->assertEquals($bucketsRead, $res['bucketsRead'][array_key_last($res['bucketsRead'])]['value']);
|
||||
$this->assertEquals($bucketsCreate, $res['bucketsCreate'][array_key_last($res['bucketsCreate'])]['value']);
|
||||
$this->assertEquals($bucketsDelete, $res['bucketsDelete'][array_key_last($res['bucketsDelete'])]['value']);
|
||||
$this->assertEquals($filesCount, $res['filesCount'][array_key_last($res['filesCount'])]['value']);
|
||||
$this->assertEquals($filesRead, $res['filesRead'][array_key_last($res['filesRead'])]['value']);
|
||||
$this->assertEquals($filesCreate, $res['filesCreate'][array_key_last($res['filesCreate'])]['value']);
|
||||
$this->assertEquals($filesDelete, $res['filesDelete'][array_key_last($res['filesDelete'])]['value']);
|
||||
|
||||
$res = $this->client->call(Client::METHOD_GET, '/storage/' . $bucketId . '/usage?range=30d', array_merge($headers, [
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-mode' => 'admin'
|
||||
]));
|
||||
$res = $res['body'];
|
||||
$this->assertEquals($storageTotal, $res['filesStorage'][array_key_last($res['filesStorage'])]['value']);
|
||||
$this->assertEquals($filesCount, $res['filesCount'][array_key_last($res['filesCount'])]['value']);
|
||||
$this->assertEquals($filesRead, $res['filesRead'][array_key_last($res['filesRead'])]['value']);
|
||||
$this->assertEquals($filesCreate, $res['filesCreate'][array_key_last($res['filesCreate'])]['value']);
|
||||
$this->assertEquals($filesDelete, $res['filesDelete'][array_key_last($res['filesDelete'])]['value']);
|
||||
|
||||
$data['requestsCount'] = $requestsCount;
|
||||
return $data;
|
||||
}
|
||||
|
||||
/** @depends testStorageStats */
|
||||
public function testDatabaseStats(array $data): array
|
||||
{
|
||||
$headers = $data['headers'];
|
||||
$projectId = $data['projectId'];
|
||||
|
||||
$databaseId = '';
|
||||
$collectionId = '';
|
||||
|
||||
$requestsCount = $data['requestsCount'];
|
||||
$databasesCount = 0;
|
||||
$databasesCreate = 0;
|
||||
$databasesRead = 0;
|
||||
$databasesDelete = 0;
|
||||
|
||||
$collectionsCount = 0;
|
||||
$collectionsCreate = 0;
|
||||
$collectionsRead = 0;
|
||||
$collectionsUpdate = 0;
|
||||
$collectionsDelete = 0;
|
||||
|
||||
$documentsCount = 0;
|
||||
$documentsCreate = 0;
|
||||
$documentsRead = 0;
|
||||
$documentsDelete = 0;
|
||||
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$name = uniqid() . ' database';
|
||||
$res = $this->client->call(Client::METHOD_POST, '/databases', $headers, [
|
||||
'databaseId' => 'unique()',
|
||||
'name' => $name,
|
||||
]);
|
||||
$this->assertEquals($name, $res['body']['name']);
|
||||
$this->assertNotEmpty($res['body']['$id']);
|
||||
$databaseId = $res['body']['$id'];
|
||||
|
||||
$requestsCount++;
|
||||
$databasesCount++;
|
||||
$databasesCreate++;
|
||||
|
||||
if ($i < 5) {
|
||||
$res = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId, $headers);
|
||||
$this->assertEquals($databaseId, $res['body']['$id']);
|
||||
$databasesRead++;
|
||||
|
||||
$res = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $headers);
|
||||
$this->assertEmpty($res['body']);
|
||||
$databasesDelete++;
|
||||
|
||||
$databasesCount--;
|
||||
$requestsCount += 2;
|
||||
}
|
||||
}
|
||||
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$name = uniqid() . ' collection';
|
||||
$res = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', $headers, [
|
||||
'collectionId' => 'unique()',
|
||||
'name' => $name,
|
||||
'permission' => 'collection',
|
||||
'read' => ['role:all'],
|
||||
'write' => ['role:all']
|
||||
]);
|
||||
$this->assertEquals($name, $res['body']['name']);
|
||||
$this->assertNotEmpty($res['body']['$id']);
|
||||
$collectionId = $res['body']['$id'];
|
||||
|
||||
$requestsCount++;
|
||||
$collectionsCount++;
|
||||
$collectionsCreate++;
|
||||
|
||||
if ($i < 5) {
|
||||
$res = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId, $headers);
|
||||
$this->assertEquals($collectionId, $res['body']['$id']);
|
||||
$collectionsRead++;
|
||||
|
||||
$res = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $collectionId, $headers);
|
||||
$this->assertEmpty($res['body']);
|
||||
$collectionsDelete++;
|
||||
|
||||
$collectionsCount--;
|
||||
$requestsCount += 2;
|
||||
}
|
||||
}
|
||||
|
||||
$res = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes' . '/string', $headers, [
|
||||
'key' => 'name',
|
||||
'size' => 255,
|
||||
'required' => true,
|
||||
]);
|
||||
$this->assertEquals('name', $res['body']['key']);
|
||||
$collectionsUpdate++;
|
||||
$requestsCount++;
|
||||
sleep(10);
|
||||
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$name = uniqid() . ' collection';
|
||||
$res = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', $headers, [
|
||||
'documentId' => 'unique()',
|
||||
'data' => ['name' => $name]
|
||||
]);
|
||||
$this->assertEquals($name, $res['body']['name']);
|
||||
$this->assertNotEmpty($res['body']['$id']);
|
||||
$documentId = $res['body']['$id'];
|
||||
|
||||
$requestsCount++;
|
||||
$documentsCount++;
|
||||
$documentsCreate++;
|
||||
|
||||
if ($i < 5) {
|
||||
$res = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, $headers);
|
||||
$this->assertEquals($documentId, $res['body']['$id']);
|
||||
$documentsRead++;
|
||||
|
||||
$res = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, $headers);
|
||||
$this->assertEmpty($res['body']);
|
||||
$documentsDelete++;
|
||||
|
||||
$documentsCount--;
|
||||
$requestsCount += 2;
|
||||
}
|
||||
}
|
||||
|
||||
sleep(35);
|
||||
|
||||
// check datbase stats
|
||||
$headers = [
|
||||
'origin' => 'http://localhost',
|
||||
'x-appwrite-project' => 'console',
|
||||
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
|
||||
];
|
||||
$res = $this->client->call(Client::METHOD_GET, '/projects/' . $projectId . '/usage?range=30d', $headers);
|
||||
$res = $res['body'];
|
||||
|
||||
$this->assertEquals(8, count($res));
|
||||
$this->assertEquals(30, count($res['requests']));
|
||||
$this->assertEquals(30, count($res['storage']));
|
||||
$this->assertEquals($requestsCount, $res['requests'][array_key_last($res['requests'])]['value']);
|
||||
$this->assertEquals($collectionsCount, $res['collections'][array_key_last($res['collections'])]['value']);
|
||||
$this->assertEquals($documentsCount, $res['documents'][array_key_last($res['documents'])]['value']);
|
||||
|
||||
$res = $this->client->call(Client::METHOD_GET, '/databases/usage?range=30d', array_merge($headers, [
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-mode' => 'admin'
|
||||
]));
|
||||
$res = $res['body'];
|
||||
$this->assertEquals($databasesCount, $res['databasesCount'][array_key_last($res['databasesCount'])]['value']);
|
||||
$this->assertEquals($collectionsCount, $res['collectionsCount'][array_key_last($res['collectionsCount'])]['value']);
|
||||
$this->assertEquals($documentsCount, $res['documentsCount'][array_key_last($res['documentsCount'])]['value']);
|
||||
|
||||
$this->assertEquals($databasesCreate, $res['databasesCreate'][array_key_last($res['databasesCreate'])]['value']);
|
||||
$this->assertEquals($databasesRead, $res['databasesRead'][array_key_last($res['databasesRead'])]['value']);
|
||||
$this->assertEquals($databasesDelete, $res['databasesDelete'][array_key_last($res['databasesDelete'])]['value']);
|
||||
|
||||
$this->assertEquals($collectionsCreate, $res['collectionsCreate'][array_key_last($res['collectionsCreate'])]['value']);
|
||||
$this->assertEquals($collectionsRead, $res['collectionsRead'][array_key_last($res['collectionsRead'])]['value']);
|
||||
$this->assertEquals($collectionsUpdate, $res['collectionsUpdate'][array_key_last($res['collectionsUpdate'])]['value']);
|
||||
$this->assertEquals($collectionsDelete, $res['collectionsDelete'][array_key_last($res['collectionsDelete'])]['value']);
|
||||
|
||||
$this->assertEquals($documentsCreate, $res['documentsCreate'][array_key_last($res['documentsCreate'])]['value']);
|
||||
$this->assertEquals($documentsRead, $res['documentsRead'][array_key_last($res['documentsRead'])]['value']);
|
||||
$this->assertEquals($documentsDelete, $res['documentsDelete'][array_key_last($res['documentsDelete'])]['value']);
|
||||
|
||||
$res = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/usage?range=30d', array_merge($headers, [
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-mode' => 'admin'
|
||||
]));
|
||||
$res = $res['body'];
|
||||
$this->assertEquals($collectionsCount, $res['collectionsCount'][array_key_last($res['collectionsCount'])]['value']);
|
||||
$this->assertEquals($documentsCount, $res['documentsCount'][array_key_last($res['documentsCount'])]['value']);
|
||||
|
||||
$this->assertEquals($collectionsCreate, $res['collectionsCreate'][array_key_last($res['collectionsCreate'])]['value']);
|
||||
$this->assertEquals($collectionsRead, $res['collectionsRead'][array_key_last($res['collectionsRead'])]['value']);
|
||||
$this->assertEquals($collectionsUpdate, $res['collectionsUpdate'][array_key_last($res['collectionsUpdate'])]['value']);
|
||||
$this->assertEquals($collectionsDelete, $res['collectionsDelete'][array_key_last($res['collectionsDelete'])]['value']);
|
||||
|
||||
$this->assertEquals($documentsCreate, $res['documentsCreate'][array_key_last($res['documentsCreate'])]['value']);
|
||||
$this->assertEquals($documentsRead, $res['documentsRead'][array_key_last($res['documentsRead'])]['value']);
|
||||
$this->assertEquals($documentsDelete, $res['documentsDelete'][array_key_last($res['documentsDelete'])]['value']);
|
||||
|
||||
$res = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/usage?range=30d', array_merge($headers, [
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-mode' => 'admin'
|
||||
]));
|
||||
$res = $res['body'];
|
||||
$this->assertEquals($documentsCount, $res['documentsCount'][array_key_last($res['documentsCount'])]['value']);
|
||||
|
||||
$this->assertEquals($documentsCreate, $res['documentsCreate'][array_key_last($res['documentsCreate'])]['value']);
|
||||
$this->assertEquals($documentsRead, $res['documentsRead'][array_key_last($res['documentsRead'])]['value']);
|
||||
$this->assertEquals($documentsDelete, $res['documentsDelete'][array_key_last($res['documentsDelete'])]['value']);
|
||||
|
||||
$data['requestsCount'] = $requestsCount;
|
||||
return $data;
|
||||
}
|
||||
|
||||
/** @depends testDatabaseStats */
|
||||
public function testFunctionsStats(array $data): void
|
||||
{
|
||||
$functionId = '';
|
||||
$requestsCount = $data['requestsCount'];
|
||||
$executionTime = 0;
|
||||
$executions = 0;
|
||||
$failures = 0;
|
||||
$compute = 0;
|
||||
|
||||
|
||||
$response1 = $this->client->call(Client::METHOD_POST, '/functions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'functionId' => 'unique()',
|
||||
'name' => 'Test',
|
||||
'runtime' => 'php-8.0',
|
||||
'vars' => [
|
||||
'funcKey1' => 'funcValue1',
|
||||
'funcKey2' => 'funcValue2',
|
||||
'funcKey3' => 'funcValue3',
|
||||
],
|
||||
'events' => [
|
||||
'users.*.create',
|
||||
'users.*.delete',
|
||||
],
|
||||
'schedule' => '0 0 1 1 *',
|
||||
'timeout' => 10,
|
||||
]);
|
||||
|
||||
$functionId = $response1['body']['$id'] ?? '';
|
||||
|
||||
$this->assertEquals(201, $response1['headers']['status-code']);
|
||||
$this->assertNotEmpty($response1['body']['$id']);
|
||||
|
||||
$requestsCount++;
|
||||
|
||||
$folder = 'php';
|
||||
$code = realpath(__DIR__ . '/../../resources/functions') . "/$folder/code.tar.gz";
|
||||
$this->packageCode($folder);
|
||||
|
||||
$deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([
|
||||
'content-type' => 'multipart/form-data',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'entrypoint' => 'index.php',
|
||||
'code' => new CURLFile($code, 'application/x-gzip', \basename($code)),
|
||||
'activate' => true
|
||||
]);
|
||||
|
||||
$deploymentId = $deployment['body']['$id'] ?? '';
|
||||
|
||||
$this->assertEquals(201, $deployment['headers']['status-code']);
|
||||
$this->assertNotEmpty($deployment['body']['$id']);
|
||||
$this->assertIsInt($deployment['body']['$createdAt']);
|
||||
$this->assertEquals('index.php', $deployment['body']['entrypoint']);
|
||||
|
||||
// Wait for deployment to build.
|
||||
sleep(30);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), []);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertNotEmpty($response['body']['$id']);
|
||||
$this->assertIsInt($response['body']['$createdAt']);
|
||||
$this->assertIsInt($response['body']['$updatedAt']);
|
||||
$this->assertEquals($deploymentId, $response['body']['deployment']);
|
||||
|
||||
$execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'async' => false,
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $execution['headers']['status-code']);
|
||||
$this->assertNotEmpty($execution['body']['$id']);
|
||||
$this->assertEquals($functionId, $execution['body']['functionId']);
|
||||
$compute += (int) ($execution['body']['time'] * 1000);
|
||||
$executionTime += (int) ($execution['body']['time'] * 1000);
|
||||
$executions++;
|
||||
|
||||
$execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'async' => true,
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $execution['headers']['status-code']);
|
||||
$this->assertNotEmpty($execution['body']['$id']);
|
||||
$this->assertEquals($functionId, $execution['body']['functionId']);
|
||||
$executionId = $execution['body']['$id'];
|
||||
$executions++;
|
||||
|
||||
//wait for execution to complete
|
||||
sleep(10);
|
||||
|
||||
$execution = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions/' . $executionId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(200, $execution['headers']['status-code']);
|
||||
$this->assertEquals($executionId, $execution['body']['$id']);
|
||||
$this->assertEquals($functionId, $execution['body']['functionId']);
|
||||
$this->assertEquals('completed', $execution['body']['status']);
|
||||
|
||||
$compute += (int) ($execution['body']['time'] * 1000);
|
||||
$executionTime += (int) ($execution['body']['time'] * 1000);
|
||||
|
||||
sleep(45);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/usage', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id']
|
||||
], $this->getHeaders()), [
|
||||
'range' => '30d'
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals(4, count($response['body']));
|
||||
$this->assertEquals('30d', $response['body']['range']);
|
||||
$this->assertIsArray($response['body']['functionsExecutions']);
|
||||
$this->assertIsArray($response['body']['functionsFailures']);
|
||||
$this->assertIsArray($response['body']['functionsCompute']);
|
||||
$response = $response['body'];
|
||||
|
||||
$this->assertEquals($executions, $response['functionsExecutions'][array_key_last($response['functionsExecutions'])]['value']);
|
||||
$this->assertGreaterThan($compute, $response['functionsCompute'][array_key_last($response['functionsCompute'])]['value']);
|
||||
$this->assertEquals($failures, $response['functionsFailures'][array_key_last($response['functionsFailures'])]['value']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/functions/usage', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id']
|
||||
], $this->getHeaders()), [
|
||||
'range' => '30d'
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals(6, count($response['body']));
|
||||
$this->assertEquals($response['body']['range'], '30d');
|
||||
$this->assertIsArray($response['body']['functionsExecutions']);
|
||||
$this->assertIsArray($response['body']['functionsFailures']);
|
||||
$this->assertIsArray($response['body']['functionsCompute']);
|
||||
$this->assertIsArray($response['body']['functionsExecutionTime']);
|
||||
$this->assertIsArray($response['body']['functionsBuildTime']);
|
||||
$response = $response['body'];
|
||||
|
||||
$this->assertEquals($executions, $response['functionsExecutions'][array_key_last($response['functionsExecutions'])]['value']);
|
||||
$this->assertGreaterThan($compute, $response['functionsCompute'][array_key_last($response['functionsCompute'])]['value']);
|
||||
$this->assertEquals($executionTime, $response['functionsExecutionTime'][array_key_last($response['functionsExecutionTime'])]['value']);
|
||||
$this->assertGreaterThan(0, $response['functionsBuildTime'][array_key_last($response['functionsBuildTime'])]['value']);
|
||||
$this->assertEquals($failures, $response['functionsFailures'][array_key_last($response['functionsFailures'])]['value']);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$this->usersCount = 0;
|
||||
$this->requestsCount = 0;
|
||||
$projectId = '';
|
||||
$headers = [];
|
||||
}
|
||||
}
|
||||
|
|
@ -12,11 +12,12 @@ trait ProjectCustom
|
|||
protected static $project = [];
|
||||
|
||||
/**
|
||||
* @param bool $fresh
|
||||
* @return array
|
||||
*/
|
||||
public function getProject(): array
|
||||
public function getProject(bool $fresh = false): array
|
||||
{
|
||||
if (!empty(self::$project)) {
|
||||
if (!empty(self::$project) && !$fresh) {
|
||||
return self::$project;
|
||||
}
|
||||
|
||||
|
|
@ -115,13 +116,17 @@ trait ProjectCustom
|
|||
$this->assertEquals(201, $webhook['headers']['status-code']);
|
||||
$this->assertNotEmpty($webhook['body']);
|
||||
|
||||
self::$project = [
|
||||
$project = [
|
||||
'$id' => $project['body']['$id'],
|
||||
'name' => $project['body']['name'],
|
||||
'apiKey' => $key['body']['secret'],
|
||||
'webhookId' => $webhook['body']['$id'],
|
||||
'signatureKey' => $webhook['body']['signatureKey'],
|
||||
];
|
||||
if ($fresh) {
|
||||
return $project;
|
||||
}
|
||||
self::$project = $project;
|
||||
|
||||
return self::$project;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue