feat: Add TeleCart Pulse heartbeat telemetry

This commit is contained in:
2025-12-03 23:03:16 +03:00
parent 772efce242
commit b60c77e453
11 changed files with 202 additions and 5 deletions

View File

@@ -1,2 +1,3 @@
APP_DEBUG=true
PULSE_API_HOST=http://host.docker.internal:8086/api/
PULSE_HEARTBEAT_SECRET=c5261f5d-529e-45ad-a69c-9778b755b7cb

View File

@@ -1,2 +1,3 @@
APP_DEBUG=false
PULSE_API_HOST=http://host.docker.internal:8086/api/
PULSE_HEARTBEAT_SECRET=c5261f5d-529e-45ad-a69c-9778b755b7cb

View File

@@ -1,2 +1,3 @@
APP_DEBUG=false
PULSE_API_HOST=https://pulse.telecart.pro/api/
PULSE_HEARTBEAT_SECRET=c5261f5d-529e-45ad-a69c-9778b755b7cb

View File

@@ -6,25 +6,41 @@ use Carbon\Carbon;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\GuzzleException;
use Openguru\OpenCartFramework\Cache\CacheInterface;
use Openguru\OpenCartFramework\Support\Arr;
use Openguru\OpenCartFramework\Support\Utils;
use Openguru\OpenCartFramework\Telegram\TelegramInitDataDecoder;
use Openguru\OpenCartFramework\Telegram\TelegramService;
use Psr\Log\LoggerInterface;
use Throwable;
class TeleCartPulseService
{
private TelegramInitDataDecoder $initDataDecoder;
private PayloadSigner $payloadSigner;
private TelegramService $telegramService;
private CacheInterface $cache;
private LoggerInterface $logger;
private ?string $apiKey;
private ?PayloadSigner $heartbeatPayloadSigner;
private ?string $moduleVersion = null;
public function __construct(
TelegramInitDataDecoder $initDataDecoder,
PayloadSigner $payloadSigner,
?string $apiKey = null
TelegramService $telegramService,
CacheInterface $cache,
LoggerInterface $logger,
?string $apiKey = null,
?PayloadSigner $heartbeatPayloadSigner = null
) {
$this->initDataDecoder = $initDataDecoder;
$this->payloadSigner = $payloadSigner;
$this->telegramService = $telegramService;
$this->cache = $cache;
$this->logger = $logger;
$this->apiKey = $apiKey;
$this->heartbeatPayloadSigner = $heartbeatPayloadSigner;
}
/**
@@ -68,6 +84,65 @@ class TeleCartPulseService
}
}
/**
* @throws GuzzleException
*/
public function handleHeartbeat(): void
{
if ($this->cache->get('telecart_pulse_heartbeat')) {
return;
}
try {
$this->cache->set('telecart_pulse_heartbeat', time(), 3600);
$me = $this->telegramService->getMe();
} catch (Throwable $e) {
$this->logger->warning(
'TeleCart Pulse heartbeat prerequisites failed: ' . $e->getMessage(),
['exception' => $e]
);
return;
}
$botName = $me['username'] ?? 'unknown';
$moduleVersion = $this->getModuleVersion();
$payload = [
'event' => 'HEARTBEAT',
'meta' => [
'domain' => Utils::getCurrentDomain(),
'bot_name' => $botName,
'php_version' => PHP_VERSION,
'module_version' => $moduleVersion,
'opencart_version' => defined('VERSION') ? VERSION : 'unknown',
'opencart_version_core' => defined('VERSION_CORE') ? VERSION_CORE : 'unknown',
],
'timestamp' => Carbon::now('UTC')->toJSON(),
];
if (! $this->heartbeatPayloadSigner) {
return;
}
try {
$signature = $this->heartbeatPayloadSigner->sign($payload);
} catch (PayloadSignException $exception) {
$this->logger->warning(
'TeleCart Pulse heartbeat signing failed: ' . $exception->getMessage(),
['exception' => $exception]
);
return;
}
$dataToSend = [
'payload' => $payload,
'signature' => $signature,
];
$this->pushHeartbeat($dataToSend);
}
/**
* @throws PayloadSignException
* @throws GuzzleException
@@ -109,13 +184,53 @@ class TeleCartPulseService
'timeout' => env('PULSE_TIMEOUT', 5.0),
'headers' => [
'Authorization' => 'Bearer ' . $this->apiKey,
'X-TELECART-VERSION' => '2.0.0',
'X-TELECART-VERSION' => $this->getModuleVersion(),
],
]);
$client->post('events', compact('json'));
}
/**
* @throws GuzzleException
*/
private function pushHeartbeat(array $json): void
{
$baseUri = rtrim(env('PULSE_API_HOST', 'http://localhost'), '/') . '/';
$client = new Client([
'base_uri' => $baseUri,
'timeout' => env('PULSE_TIMEOUT', 2.0),
'headers' => [
'X-TELECART-VERSION' => $this->getModuleVersion(),
],
]);
$client->post('heartbeat', compact('json'));
}
private function getModuleVersion(): string
{
if ($this->moduleVersion !== null) {
return $this->moduleVersion;
}
$moduleVersion = 'unknown';
$composerPath = __DIR__ . '/../../composer.json';
if (file_exists($composerPath)) {
$composerRaw = @file_get_contents($composerPath);
if ($composerRaw !== false) {
$composer = json_decode($composerRaw, true);
if (is_array($composer) && isset($composer['version'])) {
$moduleVersion = (string)$composer['version'];
}
}
}
return $this->moduleVersion = $moduleVersion;
}
private function handleOrderCreated(array $data, array $deserialized): void
{
if (isset($deserialized['campaign_id'], $deserialized['tracking_id'])) {

View File

@@ -2,9 +2,12 @@
namespace Openguru\OpenCartFramework\TeleCartPulse;
use Openguru\OpenCartFramework\Cache\CacheInterface;
use Openguru\OpenCartFramework\Container\Container;
use Openguru\OpenCartFramework\Container\ServiceProvider;
use Openguru\OpenCartFramework\Telegram\TelegramInitDataDecoder;
use Openguru\OpenCartFramework\Telegram\TelegramService;
use Psr\Log\LoggerInterface;
class TeleCartPulseServiceProvider extends ServiceProvider
{
@@ -17,10 +20,17 @@ class TeleCartPulseServiceProvider extends ServiceProvider
});
$this->container->singleton(TeleCartPulseService::class, function (Container $app) {
$heartbeatSecret = $app->getConfigValue('pulse.heartbeat_secret') ?? env('PULSE_HEARTBEAT_SECRET');
$heartbeatSigner = $heartbeatSecret ? new PayloadSigner($heartbeatSecret) : null;
return new TeleCartPulseService(
$app->get(TelegramInitDataDecoder::class),
$app->get(PayloadSigner::class),
$app->get(TelegramService::class),
$app->get(CacheInterface::class),
$app->get(LoggerInterface::class),
$app->getConfigValue('pulse.api_key'),
$heartbeatSigner,
);
});
}

View File

@@ -9,14 +9,20 @@ use Openguru\OpenCartFramework\Http\Request;
use Openguru\OpenCartFramework\Http\Response;
use Openguru\OpenCartFramework\TeleCartPulse\PulseIngestException;
use Openguru\OpenCartFramework\TeleCartPulse\TeleCartPulseService;
use Psr\Log\LoggerInterface;
use Throwable;
class TelemetryHandler
{
private TeleCartPulseService $teleCartPulseService;
private LoggerInterface $logger;
public function __construct(TeleCartPulseService $teleCartPulseService)
{
public function __construct(
TeleCartPulseService $teleCartPulseService,
LoggerInterface $logger
) {
$this->teleCartPulseService = $teleCartPulseService;
$this->logger = $logger;
}
/**
@@ -28,4 +34,15 @@ class TelemetryHandler
return new JsonResponse([], Response::HTTP_NO_CONTENT);
}
public function heartbeat(): JsonResponse
{
try {
$this->teleCartPulseService->handleHeartbeat();
} catch (Throwable $e) {
$this->logger->warning('TeleCart Pulse Heartbeat failed: ' . $e->getMessage(), ['exception' => $e]);
}
return new JsonResponse(['status' => 'ok']);
}
}

View File

@@ -24,6 +24,7 @@ return [
'getForm' => [FormsHandler::class, 'getForm'],
'health' => [HealthCheckHandler::class, 'handle'],
'ingest' => [TelemetryHandler::class, 'ingest'],
'heartbeat' => [TelemetryHandler::class, 'heartbeat'],
'manifest' => [SettingsHandler::class, 'manifest'],
'processBlock' => [BlocksHandler::class, 'processBlock'],
'product_show' => [ProductsHandler::class, 'show'],