feat: boka push ingest and stale monitoring (todo 04)
Add token- and IP-protected ingest.php, boka status checks in monitor.php, and document the two-repo push workflow. Todo 04 marked complete. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Receive push metrics from boka (POST JSON, token + IP whitelist).
|
||||
* Usage: POST /ingest.php with JSON body from push-status.sh
|
||||
*/
|
||||
|
||||
$configDir = is_dir(dirname(__DIR__) . '/private/config')
|
||||
? dirname(__DIR__) . '/private/config'
|
||||
: dirname(__DIR__) . '/config';
|
||||
|
||||
require_once $configDir . '/monitor-lib.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
function ingestError(int $code, string $message): never
|
||||
{
|
||||
http_response_code($code);
|
||||
echo json_encode(['ok' => false, 'error' => $message], JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
ingestError(405, 'method not allowed');
|
||||
}
|
||||
|
||||
$allowedIp = env('BOKA_IP');
|
||||
if ($allowedIp !== null && $allowedIp !== '') {
|
||||
$remoteAddr = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||
if (!hash_equals($allowedIp, $remoteAddr)) {
|
||||
ingestError(403, 'forbidden');
|
||||
}
|
||||
}
|
||||
|
||||
$raw = file_get_contents('php://input');
|
||||
if ($raw === false || $raw === '') {
|
||||
ingestError(400, 'empty body');
|
||||
}
|
||||
|
||||
$data = json_decode($raw, true);
|
||||
if (!is_array($data)) {
|
||||
ingestError(400, 'invalid json');
|
||||
}
|
||||
|
||||
$expectedToken = env('MONITOR_INGEST_TOKEN') ?? '';
|
||||
$token = isset($data['token']) && is_string($data['token']) ? $data['token'] : '';
|
||||
if ($expectedToken === '' || !hash_equals($expectedToken, $token)) {
|
||||
ingestError(403, 'forbidden');
|
||||
}
|
||||
|
||||
if (!isset($data['load']) || !is_array($data['load'])) {
|
||||
ingestError(400, 'missing load');
|
||||
}
|
||||
|
||||
foreach (['disk_percent', 'mem_available_mb', 'containers_running', 'containers_unhealthy'] as $field) {
|
||||
if (!isset($data[$field]) || !is_numeric($data[$field])) {
|
||||
ingestError(400, "missing {$field}");
|
||||
}
|
||||
}
|
||||
|
||||
$load = array_map(static fn (mixed $v): float => (float) $v, $data['load']);
|
||||
|
||||
$backupLastSuccess = null;
|
||||
if (isset($data['backup_last_success']) && is_string($data['backup_last_success']) && $data['backup_last_success'] !== '') {
|
||||
$backupLastSuccess = $data['backup_last_success'];
|
||||
}
|
||||
|
||||
$host = isset($data['host']) && is_string($data['host']) ? $data['host'] : 'boka';
|
||||
$clientTimestamp = isset($data['timestamp']) && is_string($data['timestamp']) ? $data['timestamp'] : null;
|
||||
|
||||
$status = [
|
||||
'last_push' => gmdate('c'),
|
||||
'received_from' => $_SERVER['REMOTE_ADDR'] ?? null,
|
||||
'host' => $host,
|
||||
'client_timestamp' => $clientTimestamp,
|
||||
'load' => $load,
|
||||
'disk_percent' => (int) $data['disk_percent'],
|
||||
'mem_available_mb' => (int) $data['mem_available_mb'],
|
||||
'containers_running' => (int) $data['containers_running'],
|
||||
'containers_unhealthy' => (int) $data['containers_unhealthy'],
|
||||
'backup_last_success' => $backupLastSuccess,
|
||||
];
|
||||
|
||||
try {
|
||||
writeJsonFile(privateDataDir() . '/boka-status.json', $status);
|
||||
} catch (Throwable $e) {
|
||||
ingestError(500, 'storage error');
|
||||
}
|
||||
|
||||
echo json_encode(['ok' => true], JSON_UNESCAPED_SLASHES);
|
||||
Reference in New Issue
Block a user