Merge branch 'feat-usage-updates' of https://github.com/appwrite/appwrite into feat-refactor-usage-stats

This commit is contained in:
Damodar Lohani 2022-08-08 01:59:37 +00:00
commit 178414cef4
24 changed files with 978 additions and 30 deletions

2
.env
View file

@ -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

View file

@ -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);
});

View file

@ -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());

View file

@ -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}}">

View file

@ -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>

View file

@ -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();
}
}
}

File diff suppressed because one or more lines are too long

View file

@ -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;}

File diff suppressed because one or more lines are too long

View file

@ -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 = {

View file

@ -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",

View file

@ -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);

View file

@ -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] ?? '';

View file

@ -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

View 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;
}
}

View file

@ -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
])
;
}

View 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 = [];
}
}

View file

@ -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;
}