appwrite/src/Appwrite/Platform/Workers/StatsUsage.php

584 lines
24 KiB
PHP
Raw Normal View History

2023-10-25 08:38:44 +00:00
<?php
namespace Appwrite\Platform\Workers;
2024-03-06 17:34:21 +00:00
use Exception;
use Throwable;
use Utopia\Console;
2025-04-15 04:48:19 +00:00
use Utopia\Database\Database;
2024-01-18 16:52:20 +00:00
use Utopia\Database\DateTime;
2023-10-25 08:38:44 +00:00
use Utopia\Database\Document;
2025-01-30 04:33:42 +00:00
use Utopia\Platform\Action;
2023-10-25 08:38:44 +00:00
use Utopia\Queue\Message;
use Utopia\Registry\Registry;
2024-04-01 11:02:47 +00:00
use Utopia\System\System;
2023-10-25 08:38:44 +00:00
2025-01-30 04:53:53 +00:00
class StatsUsage extends Action
2023-10-25 08:38:44 +00:00
{
/**
* In memory per project metrics calculation
*/
protected array $stats = [];
protected int $lastTriggeredTime = 0;
protected int $keys = 0;
protected const INFINITY_PERIOD = '_inf_';
protected const BATCH_SIZE_DEVELOPMENT = 1;
protected const BATCH_SIZE_PRODUCTION = 10_000;
2024-01-28 09:28:59 +00:00
/**
* Stats for batch write separated per project
* @var array
*/
protected array $projects = [];
/**
* Array of stat documents to batch write to logsDB
* @var array
*/
protected array $statDocuments = [];
protected Registry $register;
/**
* Metrics to skip writing to logsDB
* As these metrics are calculated separately
* by logs DB
* @var array
*/
protected array $skipBaseMetrics = [
METRIC_DATABASES => true,
METRIC_DATABASES_DOCUMENTSDB => true,
METRIC_DATABASES_VECTORSDB => true,
METRIC_BUCKETS => true,
METRIC_USERS => true,
METRIC_FUNCTIONS => true,
METRIC_TEAMS => true,
METRIC_MESSAGES => true,
METRIC_MAU => true,
METRIC_WEBHOOKS => true,
METRIC_PLATFORMS => true,
METRIC_PROVIDERS => true,
METRIC_TOPICS => true,
METRIC_KEYS => true,
METRIC_FILES => true,
METRIC_FILES_STORAGE => true,
METRIC_DEPLOYMENTS_STORAGE => true,
METRIC_BUILDS_STORAGE => true,
METRIC_DEPLOYMENTS => true,
METRIC_BUILDS => true,
METRIC_COLLECTIONS => true,
METRIC_DOCUMENTS => true,
METRIC_COLLECTIONS_DOCUMENTSDB => true,
METRIC_DOCUMENTS_DOCUMENTSDB => true,
METRIC_COLLECTIONS_VECTORSDB => true,
METRIC_DOCUMENTS_VECTORSDB => true,
METRIC_DATABASES_STORAGE => true,
METRIC_DATABASES_STORAGE_DOCUMENTSDB => true,
METRIC_DATABASES_STORAGE_VECTORSDB => true,
];
/**
* Skip metrics associated with parent IDs
* these need to be checked individually with `str_ends_with`
*/
protected array $skipParentIdMetrics = [
'.files',
'.files.storage',
'.collections',
'.documents',
'.deployments',
'.deployments.storage',
'.builds',
'.builds.storage',
'.databases.storage'
];
public const DATABASE_PREFIXES = [
DATABASE_TYPE_LEGACY,
DATABASE_TYPE_TABLESDB,
DATABASE_TYPE_DOCUMENTSDB,
];
/**
2025-04-15 04:48:19 +00:00
* @var callable(): Database
*/
protected mixed $getLogsDB;
protected array $periods = [
'1h' => 'Y-m-d H:00',
'1d' => 'Y-m-d 00:00',
'inf' => '0000-00-00 00:00'
];
2023-10-25 08:38:44 +00:00
public static function getName(): string
{
2025-02-06 04:17:49 +00:00
return 'stats-usage';
2023-10-25 08:38:44 +00:00
}
protected function getBatchSize(): int
2025-02-23 09:17:57 +00:00
{
return System::getEnv('_APP_ENV', 'development') === 'development'
? self::BATCH_SIZE_DEVELOPMENT
: self::BATCH_SIZE_PRODUCTION;
}
2023-10-25 08:38:44 +00:00
/**
* @throws Exception
*/
public function __construct()
{
2025-01-30 04:33:42 +00:00
2023-10-25 08:38:44 +00:00
$this
2025-02-06 04:17:49 +00:00
->desc('Stats usage worker')
->inject('message')
->inject('getProjectDB')
->inject('getLogsDB')
->inject('register')
2025-06-04 08:37:43 +00:00
->callback($this->action(...));
2025-01-30 04:33:42 +00:00
2024-01-28 09:28:59 +00:00
$this->lastTriggeredTime = time();
2023-10-25 08:38:44 +00:00
}
/**
* @param Message $message
2025-04-15 04:48:19 +00:00
* @param callable(): Database $getProjectDB
* @param callable(): Database $getLogsDB
* @param Registry $register
2023-10-25 08:38:44 +00:00
* @return void
* @throws \Utopia\Database\Exception
2023-12-24 18:38:15 +00:00
* @throws Exception
2023-10-25 08:38:44 +00:00
*/
public function action(Message $message, callable $getProjectDB, callable $getLogsDB, Registry $register): void
2023-10-25 08:38:44 +00:00
{
$this->getLogsDB = $getLogsDB;
$this->register = $register;
2023-10-25 08:38:44 +00:00
$payload = $message->getPayload() ?? [];
if (empty($payload)) {
throw new Exception('Missing payload');
}
2025-01-30 04:33:42 +00:00
//Todo Figure out way to preserve keys when the container is being recreated @shimonewman
2023-10-25 08:38:44 +00:00
2025-02-09 05:15:26 +00:00
$aggregationInterval = (int) System::getEnv('_APP_USAGE_AGGREGATION_INTERVAL', '20');
2025-01-30 04:33:42 +00:00
$project = new Document($payload['project'] ?? []);
2025-05-26 05:42:11 +00:00
$projectId = $project->getSequence();
// Get database type from context
$databaseContext = $payload['context']['database'] ?? null;
$databaseType = $databaseContext ? (new Document($databaseContext))->getAttribute('type', '') : '';
2023-10-25 08:38:44 +00:00
foreach ($payload['reduce'] ?? [] as $document) {
if (empty($document)) {
continue;
}
$this->reduce(
2023-12-24 18:38:15 +00:00
project: $project,
2023-10-25 08:38:44 +00:00
document: new Document($document),
metrics: $payload['metrics'],
getProjectDB: $getProjectDB,
databaseType: $databaseType
2023-10-25 08:38:44 +00:00
);
}
2025-01-30 04:33:42 +00:00
$this->stats[$projectId]['project'] = $project;
$this->stats[$projectId]['receivedAt'] = DateTime::format(new \DateTime('@' . $message->getTimestamp()));
2023-10-25 08:38:44 +00:00
foreach ($payload['metrics'] ?? [] as $metric) {
2024-03-06 17:34:21 +00:00
$this->keys++;
2024-01-28 09:28:59 +00:00
if (!isset($this->stats[$projectId]['keys'][$metric['key']])) {
$this->stats[$projectId]['keys'][$metric['key']] = $metric['value'];
2023-10-25 08:38:44 +00:00
continue;
}
2024-01-28 09:28:59 +00:00
$this->stats[$projectId]['keys'][$metric['key']] += $metric['value'];
2023-10-25 08:38:44 +00:00
}
2024-02-09 11:22:48 +00:00
// If keys crossed threshold or X time passed since the last send and there are some keys in the array ($this->stats)
2024-01-28 09:28:59 +00:00
if (
2025-02-23 09:17:57 +00:00
$this->keys >= $this->getBatchSize() ||
2025-01-30 04:33:42 +00:00
(time() - $this->lastTriggeredTime > $aggregationInterval && $this->keys > 0)
2024-01-28 09:28:59 +00:00
) {
Console::warning('[' . DateTime::now() . '] Aggregated ' . $this->keys . ' keys');
2024-02-08 19:08:29 +00:00
$this->commitToDB($getProjectDB);
2024-01-28 09:28:59 +00:00
2024-01-30 11:24:57 +00:00
$this->stats = [];
2024-01-28 09:28:59 +00:00
$this->keys = 0;
$this->lastTriggeredTime = time();
2023-10-25 08:38:44 +00:00
}
}
2024-03-06 17:34:21 +00:00
/**
* On Documents that tied by relations like functions>deployments>build || documents>collection>database || buckets>files.
* When we remove a parent document we need to deduct his children aggregation from the project scope.
* @param Document $project
* @param Document $document
* @param array $metrics
2025-04-15 04:48:19 +00:00
* @param callable(): Database $getProjectDB
* @param string $databaseType Database type from context
2024-03-06 17:34:21 +00:00
* @return void
*/
protected function reduce(Document $project, Document $document, array &$metrics, callable $getProjectDB, string $databaseType = ''): void
2023-10-25 08:38:44 +00:00
{
2023-12-24 18:38:15 +00:00
$dbForProject = $getProjectDB($project);
2023-10-25 08:38:44 +00:00
2023-12-24 18:38:15 +00:00
try {
2023-10-25 08:38:44 +00:00
switch (true) {
case $document->getCollection() === 'users': // users
$sessions = count($document->getAttribute(METRIC_SESSIONS, 0));
if (!empty($sessions)) {
$metrics[] = [
2024-03-06 17:34:21 +00:00
'key' => METRIC_SESSIONS,
'value' => ($sessions * -1),
2023-10-25 08:38:44 +00:00
];
}
break;
case $document->getCollection() === 'databases': // databases
$databaseCollectionsMetric = implode('.', array_filter([$databaseType,METRIC_COLLECTIONS]));
$databaseDocumentsMetric = implode('.', array_filter([$databaseType,METRIC_DOCUMENTS]));
$databaseIdCollectionsMetric = implode('.', array_filter([$databaseType,METRIC_DATABASE_ID_COLLECTIONS]));
$databaseIdDocumentsMetric = implode('.', array_filter([$databaseType,METRIC_DATABASE_ID_DOCUMENTS]));
$collections = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{databaseInternalId}', $document->getSequence(), $databaseIdCollectionsMetric)));
$documents = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{databaseInternalId}', $document->getSequence(), $databaseIdDocumentsMetric)));
2023-10-25 08:38:44 +00:00
if (!empty($collections['value'])) {
$metrics[] = [
'key' => $databaseCollectionsMetric,
2024-03-06 17:34:21 +00:00
'value' => ($collections['value'] * -1),
2023-10-25 08:38:44 +00:00
];
}
if (!empty($documents['value'])) {
$metrics[] = [
'key' => $databaseDocumentsMetric,
2024-03-06 17:34:21 +00:00
'value' => ($documents['value'] * -1),
2023-10-25 08:38:44 +00:00
];
}
break;
case str_starts_with($document->getCollection(), 'database_') && !str_contains($document->getCollection(), 'collection'): //collections
$databaseDocumentsMetric = implode('.', array_filter([$databaseType,METRIC_DOCUMENTS]));
$databaseIdCollectionIdDocumentsMetric = implode('.', array_filter([$databaseType,METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS]));
$databaseIdDocumentsMetric = implode('.', array_filter([$databaseType,METRIC_DATABASE_ID_DOCUMENTS]));
2023-10-25 08:38:44 +00:00
$parts = explode('_', $document->getCollection());
$databaseInternalId = $parts[1] ?? 0;
$documents = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace(
['{databaseInternalId}', '{collectionInternalId}'],
[$databaseInternalId, $document->getSequence()],
$databaseIdCollectionIdDocumentsMetric
)));
2023-10-25 08:38:44 +00:00
if (!empty($documents['value'])) {
$metrics[] = [
'key' => $databaseDocumentsMetric,
2024-03-06 17:34:21 +00:00
'value' => ($documents['value'] * -1),
2023-10-25 08:38:44 +00:00
];
$metrics[] = [
'key' => str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric),
2024-03-06 17:34:21 +00:00
'value' => ($documents['value'] * -1),
2023-10-25 08:38:44 +00:00
];
}
break;
case $document->getCollection() === 'buckets':
2025-05-26 05:42:11 +00:00
$files = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{bucketInternalId}', $document->getSequence(), METRIC_BUCKET_ID_FILES)));
$storage = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{bucketInternalId}', $document->getSequence(), METRIC_BUCKET_ID_FILES_STORAGE)));
2023-10-25 08:38:44 +00:00
if (!empty($files['value'])) {
$metrics[] = [
2024-03-06 17:34:21 +00:00
'key' => METRIC_FILES,
'value' => ($files['value'] * -1),
2023-10-25 08:38:44 +00:00
];
}
if (!empty($storage['value'])) {
$metrics[] = [
2024-03-06 17:34:21 +00:00
'key' => METRIC_FILES_STORAGE,
'value' => ($storage['value'] * -1),
2023-10-25 08:38:44 +00:00
];
}
break;
2025-03-31 06:28:58 +00:00
case $document->getCollection() === 'functions' || $document->getCollection() === 'sites':
$deployments = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getCollection(), $document->getSequence()], METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS)));
$deploymentsStorage = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getCollection(), $document->getSequence()], METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS_STORAGE)));
$builds = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getCollection(), $document->getSequence()], METRIC_RESOURCE_TYPE_ID_BUILDS)));
$buildsStorage = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getCollection(), $document->getSequence()], METRIC_RESOURCE_TYPE_ID_BUILDS_STORAGE)));
$buildsCompute = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getCollection(), $document->getSequence()], METRIC_RESOURCE_TYPE_ID_BUILDS_COMPUTE)));
$executions = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getCollection(), $document->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS)));
$executionsCompute = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getCollection(), $document->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_COMPUTE)));
2023-10-25 08:38:44 +00:00
if (!empty($deployments['value'])) {
$metrics[] = [
2024-03-06 17:34:21 +00:00
'key' => METRIC_DEPLOYMENTS,
'value' => ($deployments['value'] * -1),
2023-10-25 08:38:44 +00:00
];
2025-04-03 04:16:27 +00:00
$metrics[] = [
'key' => str_replace("{resourceType}", $document->getCollection(), METRIC_RESOURCE_TYPE_DEPLOYMENTS),
'value' => ($deployments['value'] * -1),
];
2023-10-25 08:38:44 +00:00
}
if (!empty($deploymentsStorage['value'])) {
$metrics[] = [
2024-03-06 17:34:21 +00:00
'key' => METRIC_DEPLOYMENTS_STORAGE,
'value' => ($deploymentsStorage['value'] * -1),
2023-10-25 08:38:44 +00:00
];
2025-04-03 04:16:27 +00:00
$metrics[] = [
'key' => str_replace("{resourceType}", $document->getCollection(), METRIC_RESOURCE_TYPE_DEPLOYMENTS_STORAGE),
'value' => ($deploymentsStorage['value'] * -1),
];
2023-10-25 08:38:44 +00:00
}
if (!empty($builds['value'])) {
$metrics[] = [
2024-03-06 17:34:21 +00:00
'key' => METRIC_BUILDS,
'value' => ($builds['value'] * -1),
2023-10-25 08:38:44 +00:00
];
2025-04-03 04:16:27 +00:00
$metrics[] = [
'key' => str_replace("{resourceType}", $document->getCollection(), METRIC_RESOURCE_TYPE_BUILDS),
'value' => ($builds['value'] * -1),
];
2023-10-25 08:38:44 +00:00
}
if (!empty($buildsStorage['value'])) {
$metrics[] = [
2024-03-06 17:34:21 +00:00
'key' => METRIC_BUILDS_STORAGE,
'value' => ($buildsStorage['value'] * -1),
2023-10-25 08:38:44 +00:00
];
2025-04-03 04:16:27 +00:00
$metrics[] = [
'key' => str_replace("{resourceType}", $document->getCollection(), METRIC_RESOURCE_TYPE_BUILDS_STORAGE),
'value' => ($buildsStorage['value'] * -1),
];
2023-10-25 08:38:44 +00:00
}
if (!empty($buildsCompute['value'])) {
$metrics[] = [
2024-03-06 17:34:21 +00:00
'key' => METRIC_BUILDS_COMPUTE,
'value' => ($buildsCompute['value'] * -1),
2023-10-25 08:38:44 +00:00
];
2025-04-03 04:16:27 +00:00
$metrics[] = [
'key' => str_replace("{resourceType}", $document->getCollection(), METRIC_RESOURCE_TYPE_BUILDS_COMPUTE),
'value' => ($buildsCompute['value'] * -1),
];
2023-10-25 08:38:44 +00:00
}
if (!empty($executions['value'])) {
$metrics[] = [
2024-03-06 17:34:21 +00:00
'key' => METRIC_EXECUTIONS,
'value' => ($executions['value'] * -1),
2023-10-25 08:38:44 +00:00
];
2025-04-03 04:16:27 +00:00
$metrics[] = [
'key' => str_replace("{resourceType}", $document->getCollection(), METRIC_RESOURCE_TYPE_EXECUTIONS),
'value' => ($executions['value'] * -1),
];
2023-10-25 08:38:44 +00:00
}
if (!empty($executionsCompute['value'])) {
$metrics[] = [
2024-03-06 17:34:21 +00:00
'key' => METRIC_EXECUTIONS_COMPUTE,
'value' => ($executionsCompute['value'] * -1),
2023-10-25 08:38:44 +00:00
];
2025-04-03 04:16:27 +00:00
$metrics[] = [
'key' => str_replace("{resourceType}", $document->getCollection(), METRIC_RESOURCE_TYPE_EXECUTIONS_COMPUTE),
'value' => ($executionsCompute['value'] * -1),
2023-10-25 08:38:44 +00:00
];
}
break;
default:
break;
}
2025-03-31 03:59:37 +00:00
} catch (Throwable $e) {
2025-05-26 05:42:11 +00:00
Console::error("[reducer] " . " {DateTime::now()} " . " {$project->getSequence()} " . " {$e->getMessage()}");
2023-10-25 08:38:44 +00:00
}
}
2025-04-15 04:48:19 +00:00
/**
* Commit stats to DB
* @param callable(): Database $getProjectDB
* @return void
*/
public function commitToDb(callable $getProjectDB): void
{
foreach ($this->stats as $stats) {
2025-03-31 03:55:06 +00:00
$project = $stats['project'] ?? new Document([]);
$numberOfKeys = !empty($stats['keys']) ? count($stats['keys']) : 0;
$receivedAt = $stats['receivedAt'] ?? null;
if ($numberOfKeys === 0) {
continue;
}
2025-05-26 05:42:11 +00:00
Console::log('['.DateTime::now().'] Id: '.$project->getId(). ' InternalId: '.$project->getSequence(). ' Db: '.$project->getAttribute('database').' ReceivedAt: '.$receivedAt. ' Keys: '.$numberOfKeys);
try {
foreach ($stats['keys'] ?? [] as $key => $value) {
if ($value == 0) {
continue;
}
foreach ($this->periods as $period => $format) {
$time = null;
if ($period !== 'inf') {
$time = !empty($receivedAt) ? (new \DateTime($receivedAt))->format($format) : date($format, time());
}
$id = \md5("{$time}_{$period}_{$key}");
$document = new Document([
'$id' => $id,
'period' => $period,
'time' => $time,
'metric' => $key,
'value' => $value,
'region' => System::getEnv('_APP_REGION', 'default'),
]);
2025-05-26 05:42:11 +00:00
$this->projects[$project->getSequence()]['project'] = new Document([
'$id' => $project->getId(),
2025-05-26 05:42:11 +00:00
'$sequence' => $project->getSequence(),
'database' => $project->getAttribute('database'),
]);
2025-05-26 05:42:11 +00:00
$this->projects[$project->getSequence()]['stats'][] = $document;
$this->prepareForLogsDB($project, $document);
}
}
} catch (Exception $e) {
2025-05-26 05:42:11 +00:00
Console::error('[' . DateTime::now() . '] project [' . $project->getSequence() . '] database [' . $project['database'] . '] ' . ' ' . $e->getMessage());
}
}
2025-05-26 05:42:11 +00:00
foreach ($this->projects as $sequence => $projectStats) {
if (empty($sequence)) {
continue;
}
try {
$dbForProject = $getProjectDB($projectStats['project']);
Console::log('Processing batch with ' . count($projectStats['stats']) . ' stats');
2025-09-11 07:04:42 +00:00
/**
* Sort by unique index key reduce locks/deadlocks
*/
2025-10-16 04:41:49 +00:00
usort($projectStats['stats'], function ($a, $b) use ($sequence) {
2025-09-11 07:04:42 +00:00
// Metric DESC
$cmp = strcmp($b['metric'], $a['metric']);
if ($cmp !== 0) {
return $cmp;
}
// Period ASC
$cmp = strcmp($a['period'], $b['period']);
if ($cmp !== 0) {
return $cmp;
}
// Time ASC, NULLs first
if ($a['time'] === null) {
return ($b['time'] === null) ? 0 : -1;
}
if ($b['time'] === null) {
return 1;
}
return strcmp($a['time'], $b['time']);
});
2025-10-04 09:32:01 +00:00
$dbForProject->upsertDocumentsWithIncrease('stats', 'value', $projectStats['stats']);
Console::success('Batch successfully written to DB');
} catch (Throwable $e) {
Console::error('Error processing stats: ' . $e->getMessage());
2025-09-11 07:07:45 +00:00
} finally {
unset($this->projects[$sequence]);
}
}
$this->writeToLogsDB();
}
protected function prepareForLogsDB(Document $project, Document $stat): void
{
if (System::getEnv('_APP_STATS_USAGE_DUAL_WRITING', 'disabled') === 'disabled') {
return;
}
if (array_key_exists($stat->getAttribute('metric'), $this->skipBaseMetrics)) {
return;
}
foreach ($this->skipParentIdMetrics as $skipMetric) {
$metricParts = explode('.', $stat->getAttribute('metric'));
$metric = implode('.', in_array($metricParts[0], self::DATABASE_PREFIXES) ? array_slice($metricParts, 1) : $metricParts);
if (str_ends_with($metric, $skipMetric)) {
return;
}
}
2025-04-15 03:15:24 +00:00
$documentClone = clone $stat;
$dbForLogs = ($this->getLogsDB)();
$documentClone->setAttribute('$tenant', $project->getSequence());
$this->statDocuments[] = $documentClone;
}
protected function writeToLogsDB(): void
{
if (System::getEnv('_APP_STATS_USAGE_DUAL_WRITING', 'disabled') === 'disabled') {
Console::log('Dual Writing is disabled. Skipping...');
return;
}
$dbForLogs = ($this->getLogsDB)()
->setTenant(null)
->setTenantPerDocument(true);
try {
Console::log('Processing batch with ' . count($this->statDocuments) . ' stats');
2025-09-11 07:04:42 +00:00
/**
* Sort by UNIQUE KEY "_key_metric_period_time" ("_tenant","metric" DESC,"period","time")
* Here we sort by _tenant as well because of setTenantPerDocument
*/
usort($this->statDocuments, function ($a, $b) {
// Tenant ASC
2025-09-17 05:45:23 +00:00
$cmp = $a['$tenant'] <=> $b['$tenant'];
2025-09-11 07:04:42 +00:00
if ($cmp !== 0) {
return $cmp;
}
// Metric DESC
$cmp = strcmp($b['metric'], $a['metric']);
if ($cmp !== 0) {
return $cmp;
}
// Period ASC
$cmp = strcmp($a['period'], $b['period']);
if ($cmp !== 0) {
return $cmp;
}
// Time ASC, NULLs first
if ($a['time'] === null) {
return ($b['time'] === null) ? 0 : -1;
}
if ($b['time'] === null) {
return 1;
}
return strcmp($a['time'], $b['time']);
});
2025-10-04 09:32:01 +00:00
$dbForLogs->upsertDocumentsWithIncrease(
'stats',
'value',
$this->statDocuments
);
Console::success('Usage logs pushed to Logs DB');
} catch (Throwable $th) {
Console::error($th->getMessage());
2026-02-04 06:20:19 +00:00
} finally {
// Clear statDocuments to prevent memory accumulation across batches
$this->statDocuments = [];
}
}
}