diff --git a/app/controllers/api/project.php b/app/controllers/api/project.php index ae1f9041a7..ba83b02f58 100644 --- a/app/controllers/api/project.php +++ b/app/controllers/api/project.php @@ -403,6 +403,12 @@ App::get('/v1/project/usage') 'databasesWritesTotal' => $total[METRIC_DATABASES_OPERATIONS_WRITES], 'documentsdbDatabasesReadsTotal' => $total[METRIC_DATABASES_OPERATIONS_READS_DOCUMENTSDB], 'documentsdbDatabasesWritesTotal' => $total[METRIC_DATABASES_OPERATIONS_WRITES_DOCUMENTSDB], + 'vectordbDatabasesTotal' => $total[METRIC_DATABASES_VECTORDB] ?? 0, + 'vectordbCollectionsTotal' => $total[METRIC_COLLECTIONS_VECTORDB] ?? 0, + 'vectordbDocumentsTotal' => $total[METRIC_DOCUMENTS_VECTORDB] ?? 0, + 'vectordbDatabasesStorageTotal' => $total[METRIC_DATABASES_STORAGE_VECTORDB] ?? 0, + 'vectordbDatabasesReadsTotal' => $total[METRIC_DATABASES_OPERATIONS_READS_VECTORDB] ?? 0, + 'vectordbDatabasesWritesTotal' => $total[METRIC_DATABASES_OPERATIONS_WRITES_VECTORDB] ?? 0, 'executionsBreakdown' => $executionsBreakdown, 'bucketsBreakdown' => $bucketsBreakdown, 'databasesReads' => $usage[METRIC_DATABASES_OPERATIONS_READS], @@ -410,6 +416,12 @@ App::get('/v1/project/usage') 'documentsdbDatabasesReads' => $usage[METRIC_DATABASES_OPERATIONS_READS_DOCUMENTSDB], 'documentsdbDatabasesWrites' => $usage[METRIC_DATABASES_OPERATIONS_WRITES_DOCUMENTSDB], 'documentsdbDatabasesStorage' => $usage[METRIC_DATABASES_STORAGE_DOCUMENTSDB], + 'vectordbDatabases' => $usage[METRIC_DATABASES_VECTORDB] ?? [], + 'vectordbCollections' => $usage[METRIC_COLLECTIONS_VECTORDB] ?? [], + 'vectordbDocuments' => $usage[METRIC_DOCUMENTS_VECTORDB] ?? [], + 'vectordbDatabasesStorage' => $usage[METRIC_DATABASES_STORAGE_VECTORDB] ?? [], + 'vectordbDatabasesReads' => $usage[METRIC_DATABASES_OPERATIONS_READS_VECTORDB] ?? [], + 'vectordbDatabasesWrites' => $usage[METRIC_DATABASES_OPERATIONS_WRITES_VECTORDB] ?? [], 'databasesStorageBreakdown' => $databasesStorageBreakdown, 'executionsMbSecondsBreakdown' => $executionsMbSecondsBreakdown, 'buildsMbSecondsBreakdown' => $buildsMbSecondsBreakdown, @@ -419,10 +431,14 @@ App::get('/v1/project/usage') 'authPhoneCountryBreakdown' => $authPhoneCountryBreakdown, 'imageTransformations' => $usage[METRIC_FILES_IMAGES_TRANSFORMED], 'imageTransformationsTotal' => $total[METRIC_FILES_IMAGES_TRANSFORMED], - 'embeddingsText' => $total[METRIC_EMBEDDINGS_TEXT] ?? [], - 'embeddingsTextTokens' => $total[METRIC_EMBEDDINGS_TEXT_TOTAL_TOKENS], - 'embeddingsTextDuration' => $total[METRIC_EMBEDDINGS_TEXT_TOTAL_DURATION], - 'embeddingsTextErrors' => $total[METRIC_EMBEDDINGS_TEXT_TOTAL_ERROR], + 'embeddingsText' => $usage[METRIC_EMBEDDINGS_TEXT] ?? [], + 'embeddingsTextTokens' => $usage[METRIC_EMBEDDINGS_TEXT_TOTAL_TOKENS] ?? [], + 'embeddingsTextDuration' => $usage[METRIC_EMBEDDINGS_TEXT_TOTAL_DURATION] ?? [], + 'embeddingsTextErrors' => $usage[METRIC_EMBEDDINGS_TEXT_TOTAL_ERROR] ?? [], + 'embeddingsTextTotal' => $total[METRIC_EMBEDDINGS_TEXT] ?? 0, + 'embeddingsTextTokensTotal' => $total[METRIC_EMBEDDINGS_TEXT_TOTAL_TOKENS] ?? 0, + 'embeddingsTextDurationTotal' => $total[METRIC_EMBEDDINGS_TEXT_TOTAL_DURATION] ?? 0, + 'embeddingsTextErrorsTotal' => $total[METRIC_EMBEDDINGS_TEXT_TOTAL_ERROR] ?? 0, ]), Response::MODEL_USAGE_PROJECT); }); diff --git a/src/Appwrite/Platform/Workers/StatsUsage.php b/src/Appwrite/Platform/Workers/StatsUsage.php index 09860520af..b464d4e1fb 100644 --- a/src/Appwrite/Platform/Workers/StatsUsage.php +++ b/src/Appwrite/Platform/Workers/StatsUsage.php @@ -48,6 +48,7 @@ class StatsUsage extends Action protected array $skipBaseMetrics = [ METRIC_DATABASES => true, METRIC_DATABASES_DOCUMENTSDB => true, + METRIC_DATABASES_VECTORDB => true, METRIC_BUCKETS => true, METRIC_USERS => true, METRIC_FUNCTIONS => true, @@ -67,8 +68,11 @@ class StatsUsage extends Action METRIC_BUILDS => true, METRIC_COLLECTIONS => true, METRIC_DOCUMENTS => true, + METRIC_COLLECTIONS_VECTORDB => true, + METRIC_DOCUMENTS_VECTORDB => true, METRIC_DATABASES_STORAGE => true, METRIC_DATABASES_STORAGE_DOCUMENTSDB => true, + METRIC_DATABASES_STORAGE_VECTORDB => true, ]; /** diff --git a/tests/e2e/General/UsageTest.php b/tests/e2e/General/UsageTest.php index 7660861347..49b554bfb6 100644 --- a/tests/e2e/General/UsageTest.php +++ b/tests/e2e/General/UsageTest.php @@ -1131,6 +1131,228 @@ class UsageTest extends Scope } /** @depends testDocumentsDBStats */ + public function testPrepareVectorDBStats(array $data): array + { + $documentsTotal = 0; + $collectionsTotal = 0; + $vectordbTotal = 0; + $databasesTotal = $data['databasesTotal']; + $requestsTotal = $data['requestsTotal']; + + for ($i = 0; $i < self::CREATE; $i++) { + $name = uniqid() . ' vectordb'; + + $response = $this->client->call( + Client::METHOD_POST, + '/vectordb', + array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'] + ], $this->getHeaders()), + [ + 'databaseId' => 'unique()', + 'name' => $name, + ] + ); + + $this->assertEquals($name, $response['body']['name']); + $this->assertNotEmpty($response['body']['$id']); + + $requestsTotal += 1; + $vectordbTotal += 1; + + $vectordbId = $response['body']['$id']; + + if ($i < (self::CREATE / 2)) { + $response = $this->client->call( + Client::METHOD_DELETE, + '/vectordb/' . $vectordbId, + array_merge([ + 'x-appwrite-project' => $this->getProject()['$id'] + ], $this->getHeaders()), + ); + + $this->assertEmpty($response['body']); + + $vectordbTotal -= 1; + $requestsTotal += 1; + } + } + + for ($i = 0; $i < self::CREATE; $i++) { + $name = uniqid() . ' collection'; + + $response = $this->client->call( + Client::METHOD_POST, + '/vectordb/' . $vectordbId . '/collections', + array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'] + ], $this->getHeaders()), + [ + 'collectionId' => 'unique()', + 'name' => $name, + 'dimension' => 1536, + 'documentSecurity' => false, + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ] + ); + + $this->assertEquals($name, $response['body']['name']); + $this->assertNotEmpty($response['body']['$id']); + + $requestsTotal += 1; + $collectionsTotal += 1; + + $collectionId = $response['body']['$id']; + + if ($i < (self::CREATE / 2)) { + $response = $this->client->call( + Client::METHOD_DELETE, + '/vectordb/' . $vectordbId . '/collections/' . $collectionId, + array_merge([ + 'x-appwrite-project' => $this->getProject()['$id'] + ], $this->getHeaders()), + ); + + $this->assertEmpty($response['body']); + + $collectionsTotal -= 1; + $requestsTotal += 1; + } + } + + for ($i = 0; $i < self::CREATE; $i++) { + $response = $this->client->call( + Client::METHOD_POST, + '/vectordb/' . $vectordbId . '/collections/' . $collectionId . '/documents', + array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'] + ], $this->getHeaders()), + [ + 'documentId' => 'unique()', + 'data' => [ + 'embeddings' => array_fill(0, 1536, 0.1), + 'metadata' => [ + 'name' => uniqid() . ' document', + 'value' => $i + ] + ] + ] + ); + + $this->assertNotEmpty($response['body']['$id']); + + $requestsTotal += 1; + $documentsTotal += 1; + + $documentId = $response['body']['$id']; + + if ($i < (self::CREATE / 2)) { + $response = $this->client->call( + Client::METHOD_DELETE, + '/vectordb/' . $vectordbId . '/collections/' . $collectionId . '/documents/' . $documentId, + array_merge([ + 'x-appwrite-project' => $this->getProject()['$id'] + ], $this->getHeaders()), + ); + + $this->assertEmpty($response['body']); + + $documentsTotal -= 1; + $requestsTotal += 1; + } + } + + return array_merge($data, [ + 'vectordbId' => $vectordbId, + 'vectordbCollectionId' => $collectionId, + 'requestsTotal' => $requestsTotal, + 'databasesTotal' => $databasesTotal, + 'vectordbTotal' => $vectordbTotal, + 'vectordbCollectionsTotal' => $collectionsTotal, + 'vectordbDocumentsTotal' => $documentsTotal, + ]); + } + + /** @depends testPrepareVectorDBStats */ + #[Retry(count: 1)] + public function testVectorDBStats(array $data): array + { + $vectordbId = $data['vectordbId']; + $collectionId = $data['vectordbCollectionId']; + $requestsTotal = $data['requestsTotal']; + $databasesTotal = $data['databasesTotal']; + $vectordbTotal = $data['vectordbTotal']; + $collectionsTotal = $data['vectordbCollectionsTotal']; + $documentsTotal = $data['vectordbDocumentsTotal']; + + $this->assertEventually(function () use ($requestsTotal, $vectordbTotal, $documentsTotal) { + $response = $this->client->call( + Client::METHOD_GET, + '/project/usage', + $this->getConsoleHeaders(), + [ + 'period' => '1d', + 'startDate' => self::getToday(), + 'endDate' => self::getTomorrow(), + ] + ); + + $this->assertGreaterThanOrEqual(31, count($response['body'])); + $this->assertCount(1, $response['body']['requests']); + $this->assertCount(1, $response['body']['network']); + $this->assertEquals($requestsTotal, $response['body']['requests'][array_key_last($response['body']['requests'])]['value']); + $this->validateDates($response['body']['requests']); + // vectordbTotal should reflect only VectorDB instances, not relational databases. + $this->assertEquals($vectordbTotal, $response['body']['vectordbDatabasesTotal']); + $this->assertEquals($documentsTotal, $response['body']['vectordbDocumentsTotal']); + }); + + $response = $this->client->call( + Client::METHOD_GET, + '/databases/usage?range=30d', + $this->getConsoleHeaders() + ); + + $this->assertEquals($databasesTotal, $response['body']['databases'][array_key_last($response['body']['databases'])]['value']); + $this->validateDates($response['body']['databases']); + + $this->assertEventually(function () use ($vectordbId, $collectionsTotal, $documentsTotal) { + $response = $this->client->call( + Client::METHOD_GET, + '/vectordb/' . $vectordbId . '/usage?range=30d', + $this->getConsoleHeaders() + ); + + $this->assertEquals($collectionsTotal, $response['body']['collections'][array_key_last($response['body']['collections'])]['value']); + $this->validateDates($response['body']['collections']); + + $this->assertEquals($documentsTotal, $response['body']['documents'][array_key_last($response['body']['documents'])]['value']); + $this->validateDates($response['body']['documents']); + }); + + $this->assertEventually(function () use ($vectordbId, $collectionId, $documentsTotal) { + $response = $this->client->call( + Client::METHOD_GET, + '/vectordb/' . $vectordbId . '/collections/' . $collectionId . '/usage?range=30d', + $this->getConsoleHeaders() + ); + + $this->assertEquals($documentsTotal, $response['body']['documents'][array_key_last($response['body']['documents'])]['value']); + $this->validateDates($response['body']['documents']); + }); + + return $data; + } + + /** @depends testVectorDBStats */ public function testPrepareFunctionsStats(array $data): array { $executionTime = 0; @@ -1611,7 +1833,7 @@ class UsageTest extends Scope 'x-appwrite-key' => $this->getProject()['apiKey'], ], $this->getHeaders()), [ - 'embeddingModel' => 'embeddinggemma', + 'model' => 'embeddinggemma', 'texts' => [ 'usage test text ' . $i, ], @@ -1646,10 +1868,24 @@ class UsageTest extends Scope $this->assertArrayHasKey('embeddingsTextErrors', $response['body']); $this->assertArrayHasKey('embeddingsTextTokens', $response['body']); $this->assertArrayHasKey('embeddingsTextDuration', $response['body']); - $this->assertGreaterThan(0, $response['body']['embeddingsText']); - $this->assertGreaterThanOrEqual(0, $response['body']['embeddingsTextErrors']); - $this->assertGreaterThan(0, $response['body']['embeddingsTextTokens']); - $this->assertGreaterThan(0, $response['body']['embeddingsTextDuration']); + $this->assertArrayHasKey('embeddingsTextTotal', $response['body']); + $this->assertArrayHasKey('embeddingsTextErrorsTotal', $response['body']); + $this->assertArrayHasKey('embeddingsTextTokensTotal', $response['body']); + $this->assertArrayHasKey('embeddingsTextDurationTotal', $response['body']); + + // Time-series arrays should be non-empty + $this->assertNotEmpty($response['body']['embeddingsText']); + $this->assertNotEmpty($response['body']['embeddingsTextTokens']); + $this->assertNotEmpty($response['body']['embeddingsTextDuration']); + $this->validateDates($response['body']['embeddingsText']); + $this->validateDates($response['body']['embeddingsTextTokens']); + $this->validateDates($response['body']['embeddingsTextDuration']); + + // Total scalars should be greater than 0 (or >= 0 for errors) + $this->assertGreaterThan(0, $response['body']['embeddingsTextTotal']); + $this->assertGreaterThanOrEqual(0, $response['body']['embeddingsTextErrorsTotal']); + $this->assertGreaterThan(0, $response['body']['embeddingsTextTokensTotal']); + $this->assertGreaterThan(0, $response['body']['embeddingsTextDurationTotal']); }); }