feat: maintenance tasks, logs

- add interval for periodic maintenance tasks
- add cache prune periodic task
- use rotating handler for monolog
- update UI logs component
- correctly reset cache from admin
- increase cache timeout for tg data
- fix UI errors in admin
This commit is contained in:
2025-11-20 09:07:33 +03:00
parent 984d4d7ac3
commit ae9771dec4
15 changed files with 170 additions and 60 deletions

View File

@@ -1,16 +1,13 @@
<template> <template>
<textarea v-text="rows" rows="40" class="tw:w-full"/> <textarea v-text="logs.lines" rows="40" class="tw:w-full" readonly/>
</template> </template>
<script setup> <script setup>
import {onMounted, ref} from "vue"; import {onMounted} from "vue";
import {apiGet} from "@/utils/http.js"; import {useLogsStore} from "@/stores/logs.js";
const rows = ref(''); const logs = useLogsStore();
onMounted(async () => { onMounted(async () => logs.fetchLogsFromServer());
const response = await apiGet('getLogs');
rows.value = response.data;
});
</script> </script>
<style scoped> <style scoped>

View File

@@ -65,27 +65,30 @@
</div> </div>
</div> </div>
<div class="tw:mt-6 tw:lg:mt-0 tw:flex tw:items-center tw:gap-4"> <div class="tw:mt-6 tw:lg:mt-0 tw:flex tw:items-center tw:gap-4">
<ResetCacheBtn/>
<ButtonGroup> <ButtonGroup>
<ResetCacheBtn/> <Button
</ButtonGroup> icon="fa fa-play"
<div class="btn-group"> v-tooltip.top="(tgMe?.result?.has_main_web_app !== true) ? 'Вы не привязали Telegram Mini App к боту.' : 'Открыть Telegram магазин'"
<a as="a"
class="btn btn-primary"
:class="{'disabled': (tgMe?.result?.has_main_web_app !== true)}"
rounded
:href="`https://t.me/${tgMe?.result?.username}?startapp`"
target="_blank" target="_blank"
:title="(tgMe?.result?.has_main_web_app !== true) ? 'Вы не привязали Telegram Mini App к боту.' : 'Открыть Telegram магазин'" :href="`https://t.me/${tgMe?.result?.username}?startapp`"
> />
<i class="fa fa-play"></i> <Button
</a> icon="fa fa-book"
<a class="btn btn-default" target="_blank" href="https://telecart-labs.github.io/docs/" title="Документация по модулю TeleCart"> v-tooltip.top="'Документация по модулю TeleCart'"
<i class="fa fa-book"></i> as="a"
</a> target="_blank"
<a class="btn btn-default" target="_blank" href="https://t.me/ocstore3" title="Официальная Telegram группа модуля TeleCart"> href="https://telecart-labs.github.io/docs/"
<i class="fa fa-group"></i> />
</a> <Button
</div> icon="fa fa-group"
v-tooltip.top="'Официальная Telegram группа модуля TeleCart'"
as="a"
target="_blank"
href="https://t.me/ocstore3"
/>
</ButtonGroup>
</div> </div>
</div> </div>
</div> </div>
@@ -98,6 +101,7 @@ import {onMounted, ref} from "vue";
import OcImagePicker from "@/components/OcImagePicker.vue"; import OcImagePicker from "@/components/OcImagePicker.vue";
import {apiGet} from "@/utils/http.js"; import {apiGet} from "@/utils/http.js";
import ResetCacheBtn from "@/components/Form/ResetCacheBtn.vue"; import ResetCacheBtn from "@/components/Form/ResetCacheBtn.vue";
import {Button, ButtonGroup} from "primevue";
const settings = useSettingsStore(); const settings = useSettingsStore();
const stats = useStatsStore(); const stats = useStatsStore();

View File

@@ -0,0 +1,16 @@
import {defineStore} from "pinia";
import {apiGet} from "@/utils/http.js";
export const useLogsStore = defineStore('logs', {
state: () => ({
lines: '',
}),
actions: {
async fetchLogsFromServer() {
if (this.lines) return;
const response = await apiGet('getLogs');
this.lines = response.data;
},
},
});

View File

@@ -2,7 +2,7 @@
use Bastion\ApplicationFactory; use Bastion\ApplicationFactory;
use Cart\User; use Cart\User;
use Monolog\Handler\StreamHandler; use Monolog\Handler\RotatingFileHandler;
use Monolog\Logger; use Monolog\Logger;
use Openguru\OpenCartFramework\Application; use Openguru\OpenCartFramework\Application;
use Openguru\OpenCartFramework\Http\Response as HttpResponse; use Openguru\OpenCartFramework\Http\Response as HttpResponse;
@@ -252,7 +252,7 @@ class ControllerExtensionModuleTgshop extends Controller
'language_id' => (int) $this->config->get('config_language_id'), 'language_id' => (int) $this->config->get('config_language_id'),
], ],
'logs' => [ 'logs' => [
'path' => DIR_LOGS . '/telecart.log', 'path' => DIR_LOGS,
], ],
'database' => [ 'database' => [
'host' => DB_HOSTNAME, 'host' => DB_HOSTNAME,
@@ -298,10 +298,9 @@ class ControllerExtensionModuleTgshop extends Controller
{ {
$log = new Logger('TeleCart_Admin'); $log = new Logger('TeleCart_Admin');
$log->pushHandler( $log->pushHandler(
new StreamHandler( new RotatingFileHandler(
DIR_LOGS . '/telecart.log', DIR_LOGS . '/telecart.log', 14, $debug ? Logger::DEBUG : Logger::INFO
$debug ? Logger::DEBUG : Logger::INFO, ),
)
); );
return $log; return $log;

View File

@@ -5,7 +5,7 @@ use App\ApplicationFactory;
use Cart\Cart; use Cart\Cart;
use Cart\Currency; use Cart\Currency;
use Cart\Tax; use Cart\Tax;
use Monolog\Handler\StreamHandler; use Monolog\Handler\RotatingFileHandler;
use Monolog\Logger; use Monolog\Logger;
use Openguru\OpenCartFramework\Http\Response as HttpResponse; use Openguru\OpenCartFramework\Http\Response as HttpResponse;
use Openguru\OpenCartFramework\ImageTool\ImageTool; use Openguru\OpenCartFramework\ImageTool\ImageTool;
@@ -154,10 +154,7 @@ class ControllerExtensionTgshopHandle extends Controller
{ {
$log = new Logger($app); $log = new Logger($app);
$log->pushHandler( $log->pushHandler(
new StreamHandler( new RotatingFileHandler(DIR_LOGS . '/telecart.log', 14, $appDebug ? Logger::DEBUG : Logger::INFO),
DIR_LOGS . '/telecart.log',
$appDebug ? Logger::DEBUG : Logger::INFO,
)
); );
return $log; return $log;

View File

@@ -19,9 +19,15 @@ class LogsHandler
public function getLogs(): JsonResponse public function getLogs(): JsonResponse
{ {
$logsPath = $this->settings->get('logs.path'); $data = [];
$data = implode(PHP_EOL, $this->readLastLogsRows($logsPath)); $logsPath = $this->findLastLogsFileInDir(
$this->settings->get('logs.path')
);
if ($logsPath) {
$data = implode(PHP_EOL, $this->readLastLogsRows($logsPath, 100));
}
return new JsonResponse(compact('data')); return new JsonResponse(compact('data'));
} }
@@ -54,6 +60,13 @@ class LogsHandler
$linesArray = explode("\n", $chunk); $linesArray = explode("\n", $chunk);
return array_reverse(array_slice($linesArray, -$lines)); return array_slice($linesArray, -$lines);
}
private function findLastLogsFileInDir(string $dir): ?string
{
$files = glob($dir . '/telecart-*.log');
return $files ? end($files) : null;
} }
} }

View File

@@ -12,6 +12,7 @@ use Openguru\OpenCartFramework\Http\JsonResponse;
use Openguru\OpenCartFramework\Http\Request; use Openguru\OpenCartFramework\Http\Request;
use Openguru\OpenCartFramework\Http\Response; use Openguru\OpenCartFramework\Http\Response;
use Openguru\OpenCartFramework\Support\Arr; use Openguru\OpenCartFramework\Support\Arr;
use Psr\Log\LoggerInterface;
class SettingsHandler class SettingsHandler
{ {
@@ -19,17 +20,20 @@ class SettingsHandler
private Settings $settings; private Settings $settings;
private SettingsService $settingsUpdateService; private SettingsService $settingsUpdateService;
private CacheInterface $cache; private CacheInterface $cache;
private LoggerInterface $logger;
public function __construct( public function __construct(
BotTokenConfigurator $botTokenConfigurator, BotTokenConfigurator $botTokenConfigurator,
Settings $settings, Settings $settings,
SettingsService $settingsUpdateService, SettingsService $settingsUpdateService,
CacheInterface $cache CacheInterface $cache,
LoggerInterface $logger
) { ) {
$this->botTokenConfigurator = $botTokenConfigurator; $this->botTokenConfigurator = $botTokenConfigurator;
$this->settings = $settings; $this->settings = $settings;
$this->settingsUpdateService = $settingsUpdateService; $this->settingsUpdateService = $settingsUpdateService;
$this->cache = $cache; $this->cache = $cache;
$this->logger = $logger;
} }
public function configureBotToken(Request $request): JsonResponse public function configureBotToken(Request $request): JsonResponse
@@ -78,7 +82,9 @@ class SettingsHandler
public function resetCache(): JsonResponse public function resetCache(): JsonResponse
{ {
$this->cache->prune(); $this->cache->clear();
$this->logger->info('Cache cleared manually.');
return new JsonResponse([], Response::HTTP_ACCEPTED); return new JsonResponse([], Response::HTTP_ACCEPTED);
} }

View File

@@ -131,7 +131,7 @@ class TelegramHandler
if (! $data) { if (! $data) {
$data = $this->telegramService->exec('getMe'); $data = $this->telegramService->exec('getMe');
$this->cache->set('tg_me_info', $data, 60); $this->cache->set('tg_me_info', $data, 60 * 5);
} }
return new JsonResponse(compact('data')); return new JsonResponse(compact('data'));

View File

@@ -0,0 +1,30 @@
<?php
namespace Bastion\Tasks;
use DateInterval;
use Openguru\OpenCartFramework\Cache\CacheInterface;
use Openguru\OpenCartFramework\MaintenanceTasks\BaseMaintenanceTask;
use Psr\Log\LoggerInterface;
class CachePruneTask extends BaseMaintenanceTask
{
private CacheInterface $cache;
public function __construct(LoggerInterface $logger, CacheInterface $cache)
{
parent::__construct($logger);
$this->cache = $cache;
}
public function handle(): void
{
$this->cache->prune();
}
public function interval(): ?DateInterval
{
return new DateInterval('P1D');
}
}

View File

@@ -2,12 +2,13 @@
namespace Bastion\Tasks; namespace Bastion\Tasks;
use DateInterval;
use Exception; use Exception;
use JsonException; use JsonException;
use Openguru\OpenCartFramework\MaintenanceTasks\BaseMaintenanceTask; use Openguru\OpenCartFramework\MaintenanceTasks\BaseMaintenanceTask;
use RuntimeException; use RuntimeException;
class CleanUpOldAssets extends BaseMaintenanceTask class CleanUpOldAssetsTask extends BaseMaintenanceTask
{ {
public function handle(): void public function handle(): void
{ {
@@ -61,8 +62,12 @@ class CleanUpOldAssets extends BaseMaintenanceTask
} catch (JsonException $e) { } catch (JsonException $e) {
$this->logger->error('Ошибка декодирования файла manifest.json: ' . $e->getMessage()); $this->logger->error('Ошибка декодирования файла manifest.json: ' . $e->getMessage());
} catch (Exception $e) { } catch (Exception $e) {
$this->logger->error('Ошибка удаления старых assets: ' . $e->getMessage()); $this->logger->error('Ошибка удаления старых assets: ' . $e->getMessage(), ['exception' => $e]);
$this->logger->logException($e);
} }
} }
public function interval(): ?DateInterval
{
return new DateInterval('PT1H');
}
} }

View File

@@ -1,9 +1,11 @@
<?php <?php
use Bastion\Tasks\CleanUpOldAssets; use Bastion\Tasks\CachePruneTask;
use Bastion\Tasks\CleanUpOldAssetsTask;
return [ return [
'tasks' => [ 'tasks' => [
CleanUpOldAssets::class, CleanUpOldAssetsTask::class,
CachePruneTask::class,
], ],
]; ];

View File

@@ -2,6 +2,7 @@
namespace Openguru\OpenCartFramework\MaintenanceTasks; namespace Openguru\OpenCartFramework\MaintenanceTasks;
use DateInterval;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
abstract class BaseMaintenanceTask implements MaintenanceTaskInterface abstract class BaseMaintenanceTask implements MaintenanceTaskInterface
@@ -12,4 +13,9 @@ abstract class BaseMaintenanceTask implements MaintenanceTaskInterface
{ {
$this->logger = $logger; $this->logger = $logger;
} }
public function interval(): ?DateInterval
{
return null;
}
} }

View File

@@ -2,7 +2,11 @@
namespace Openguru\OpenCartFramework\MaintenanceTasks; namespace Openguru\OpenCartFramework\MaintenanceTasks;
use DateInterval;
interface MaintenanceTaskInterface interface MaintenanceTaskInterface
{ {
public function handle(): void; public function handle(): void;
public function interval(): ?DateInterval;
} }

View File

@@ -2,22 +2,27 @@
namespace Openguru\OpenCartFramework\MaintenanceTasks; namespace Openguru\OpenCartFramework\MaintenanceTasks;
use Psr\Log\LoggerInterface; use Carbon\Carbon;
use Openguru\OpenCartFramework\Cache\CacheInterface;
use Openguru\OpenCartFramework\Migrations\MigratorService; use Openguru\OpenCartFramework\Migrations\MigratorService;
use Psr\Log\LoggerInterface;
class MaintenanceTasksService class MaintenanceTasksService
{ {
private MigratorService $migrator; private MigratorService $migrator;
private LoggerInterface $logger; private LoggerInterface $logger;
private CacheInterface $cache;
private array $maintenanceTasks; private array $maintenanceTasks;
public function __construct( public function __construct(
MigratorService $migrator, MigratorService $migrator,
LoggerInterface $logger, LoggerInterface $logger,
CacheInterface $cache,
array $maintenanceTasks = [] array $maintenanceTasks = []
) { ) {
$this->migrator = $migrator; $this->migrator = $migrator;
$this->logger = $logger; $this->logger = $logger;
$this->cache = $cache;
$this->maintenanceTasks = $maintenanceTasks; $this->maintenanceTasks = $maintenanceTasks;
} }
@@ -30,18 +35,42 @@ class MaintenanceTasksService
private function performMaintenanceTasks(): void private function performMaintenanceTasks(): void
{ {
foreach ($this->maintenanceTasks as $maintenanceTask) { foreach ($this->maintenanceTasks as $maintenanceTask) {
$startTime = microtime(true);
/** @var MaintenanceTaskInterface $instance */ /** @var MaintenanceTaskInterface $instance */
$instance = $maintenanceTask(); $instance = $maintenanceTask();
$instance->handle();
$endTime = microtime(true); if ($this->shouldPerformTask($instance)) {
$this->logger->info( $startTime = microtime(true);
sprintf( $instance->handle();
'Maintenance task %s executed by %d seconds', $endTime = microtime(true);
get_class($instance), $this->logger->info(
$endTime - $startTime sprintf(
), 'Maintenance task %s executed by %d seconds',
); get_class($instance),
$endTime - $startTime
),
);
}
} }
} }
private function shouldPerformTask(MaintenanceTaskInterface $task): bool
{
$cacheKey = 'maintenance_tasks.' . md5(get_class($task));
$lastExecuted = $this->cache->get($cacheKey);
if (!$lastExecuted) {
$this->cache->set($cacheKey, Carbon::now());
return true;
}
$last = Carbon::parse($lastExecuted);
$next = $last->copy()->add($task->interval());
if (Carbon::now()->gte($next)) {
$this->cache->set($cacheKey, Carbon::now());
return true;
}
return false;
}
} }

View File

@@ -2,6 +2,7 @@
namespace Openguru\OpenCartFramework\MaintenanceTasks; namespace Openguru\OpenCartFramework\MaintenanceTasks;
use Openguru\OpenCartFramework\Cache\CacheInterface;
use Openguru\OpenCartFramework\Container\Container; use Openguru\OpenCartFramework\Container\Container;
use Openguru\OpenCartFramework\Container\ServiceProvider; use Openguru\OpenCartFramework\Container\ServiceProvider;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@@ -19,6 +20,7 @@ class MaintenanceTasksServiceProvider extends ServiceProvider
return new MaintenanceTasksService( return new MaintenanceTasksService(
$container->get(MigratorService::class), $container->get(MigratorService::class),
$container->get(LoggerInterface::class), $container->get(LoggerInterface::class),
$container->get(CacheInterface::class),
$tasks, $tasks,
); );
}); });