diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 21b827042d..fed610890b 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -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,107 @@ App::get('/v1/functions/:functionId/usage') ]); } + $response->dynamic($usage, Response::MODEL_USAGE_FUNCTION); + }); + +App::get('/v1/functions/usage') + ->desc('Get Function 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 $functionId, string $range, Response $response, Database $dbForProject) { + + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception('Function not found', 404, Exception::FUNCTION_NOT_FOUND); + } + + $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); }); diff --git a/src/Appwrite/Stats/Usage.php b/src/Appwrite/Stats/Usage.php index c03cfcebef..fcde1c1a17 100644 --- a/src/Appwrite/Stats/Usage.php +++ b/src/Appwrite/Stats/Usage.php @@ -177,6 +177,18 @@ 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_functions_executions_all', 'groupBy' => ['functionId'], diff --git a/src/Appwrite/Stats/UsageDB.php b/src/Appwrite/Stats/UsageDB.php index 3b282a81be..023f96df2c 100644 --- a/src/Appwrite/Stats/UsageDB.php +++ b/src/Appwrite/Stats/UsageDB.php @@ -234,7 +234,7 @@ class UsageDB extends Usage $executionTotal = $this->sum($projectId, 'executions', 'time', 'functions.executionTime'); $buildTotal = $this->sum($projectId, 'builds', 'duration', 'functions.buildTime'); - $this->createOrUpdateMetric($projectId, 'functions.compute', $executionTotal + $buildTotal); + $this->createOrUpdateMetric($projectId, 'functions.compute', ($executionTotal * 1000) + ($buildTotal * 1000)); //in ms } /** diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 3ca3dbb3c4..7282f820a0 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -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 diff --git a/src/Appwrite/Utopia/Response/Model/UsageFunction.php b/src/Appwrite/Utopia/Response/Model/UsageFunction.php new file mode 100644 index 0000000000..8467bc25f4 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/UsageFunction.php @@ -0,0 +1,62 @@ +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; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/UsageFunctions.php b/src/Appwrite/Utopia/Response/Model/UsageFunctions.php index 48ba65fa21..e5cad4e489 100644 --- a/src/Appwrite/Utopia/Response/Model/UsageFunctions.php +++ b/src/Appwrite/Utopia/Response/Model/UsageFunctions.php @@ -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 + ]) ; } diff --git a/tests/e2e/General/UsageTest.php b/tests/e2e/General/UsageTest.php index 0a7e4b7fe9..5f91ad2a07 100644 --- a/tests/e2e/General/UsageTest.php +++ b/tests/e2e/General/UsageTest.php @@ -414,15 +414,9 @@ class UsageTest extends Scope public function testFunctionsStats(): void { - // create some functions - // create some successful deployments and some - // failed deployments - // execute some functions - // some failed executions - // test the stats $functionId = ''; $requestsCount = 0; - $deploymentsTotal = 0; + $executionTime = 0; $executions = 0; $failures = 0; $compute = 0; @@ -488,27 +482,28 @@ class UsageTest extends Scope $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 += $execution['body']['time'] * 1000; + $executionTime += $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']); @@ -517,17 +512,18 @@ class UsageTest extends Scope //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']); $compute += $execution['body']['time'] * 1000; + $executionTime += $execution['body']['time'] * 1000; sleep(45); @@ -538,9 +534,9 @@ class UsageTest extends Scope 'range' => '30d' ]); - $this->assertEquals($response['headers']['status-code'], 200); - $this->assertEquals(count($response['body']), 4); - $this->assertEquals($response['body']['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']); @@ -550,6 +546,28 @@ class UsageTest extends Scope $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['functionsCompute'])]['value']); + $this->assertGreaterThan(0, $response['functionsBuildTime'][array_key_last($response['functionsCompute'])]['value']); + $this->assertEquals($failures, $response['functionsFailures'][array_key_last($response['functionsFailures'])]['value']); } protected function tearDown(): void