Wstęp
Poniższy opis jest częścią większej całości https://itadmin.vblog.ovh/klaster-okd-4-19-openshift-monitorowanie-serwera-pve-9-i-klastra-okd-projekt-mors/
Wymaga przygotowania klastra OKD 4.19 oraz serwera baz danych PostgreSQL
- https://itadmin.vblog.ovh/klaster-okd-4-19-openshift-na-proxmox-9-opis-instalacjii-konfiguracji/
- https://itadmin.vblog.ovh/klaster-okd-4-19-openshift-na-proxmox-9-opis-postgresql-baza-danych-dla-aplikacji-openshift/
Prawidłowa kolejność przygotowania projektu MORS na klastrze OKD 4.19
- https://itadmin.vblog.ovh/klaster-okd-4-19-openshift-projekt-monitorujacy-serwer-pve-9-i-klaster-okd-morsusl-aplikacja-mors-usl-proxmox/
- https://itadmin.vblog.ovh/klaster-okd-4-19-openshift-projekt-monitorujacy-serwer-pve-9-i-klaster-okd-morsusl-aplikacja-mors-usl-okd/
- https://itadmin.vblog.ovh/klaster-okd-4-19-openshift-projekt-monitorujacy-serwer-pve-9-i-klaster-okd-morsgui-aplikacja-mors-gui-api/
- https://itadmin.vblog.ovh/klaster-okd-4-19-openshift-projekt-monitorujacy-serwer-pve-9-i-klaster-okd-projekt-morsgui-aplikacja-mors-gui-web/
Opis aplikacji mors-gui-web

Aplikacja służy do wyświetlania wykresów na podstawie danych umieszczonych w bazie danych PostgreSQL: dba_mors.
Dane są generowane przez 2 aplikacje
- https://itadmin.vblog.ovh/klaster-okd-4-19-openshift-projekt-monitorujacy-serwer-pve-9-i-klaster-okd-morsusl-aplikacja-mors-usl-proxmox/
- https://itadmin.vblog.ovh/klaster-okd-4-19-openshift-projekt-monitorujacy-serwer-pve-9-i-klaster-okd-morsusl-aplikacja-mors-usl-okd/
i wczytywane za pomocą API w aplikacji
Kod aplikacji mors-gui-api czyli zawartość index.php ze względu na swoją długość znajduje się na samym końcu tego wpisu.
Struktura aplikacji mors-gui-web
/repo/pakiety/morsgui/1.0.0/mors-gui-web drwxr-xr-x 4 bastuser bastuser 46 Feb 13 17:49 .. -rw-r--r-- 1 bastuser bastuser 565 Feb 13 17:51 cm-mors-gui-web.yaml -rw-r--r-- 1 bastuser bastuser 380 Feb 13 18:00 Dockerfile -rw-r--r-- 1 bastuser bastuser 23030 Feb 13 18:18 index.php
Przygotowanie configmapy dla aplikacji mors-gui-web
apiVersion: v1 kind: ConfigMap metadata: name: cm-mors-gui-web namespace: morsgui-prod data: # UI / wykresy PAGE_TITLE: "morsusl-gui • mors-gui-web" DEFAULT_RANGE_HOURS: "2" # domyślne okno czasu po wejściu na stronę MAX_RANGE_HOURS: "72" # twardy limit na zakres (żeby nie zajechać DB i frontu) MAX_POINTS: "1500" # downsampling: max punktów na serię REFRESH_SECONDS: "10" # co ile sekund odświeżać (zapisy są co 5s; 10s jest sensowne) DEBUG: "0" # 1 pokaże trochę info na dole strony
Przygotowanie Dockerfile na potrzeby zbudowania obrazu aplikacji
FROM php:8.2-cli-alpine # Runtime libs dla pdo_pgsql (libpq.so.5) RUN apk add --no-cache libpq # Build deps tylko na czas kompilacji rozszerzenia RUN apk add --no-cache --virtual .build-deps postgresql-dev \ && docker-php-ext-install pdo_pgsql \ && apk del .build-deps WORKDIR /app COPY index.php /app/index.php EXPOSE 8080 CMD ["php", "-S", "0.0.0.0:8080", "-t", "/app"]
Uruchomienie aplikacji mors-gui-web w projekcie mors-gui-prod
oc new-project morsgui-prod # Build binarny z Dockerfile oc new-build --strategy=docker --binary --name=mors-gui-web-build cd /repo/pakiety/morsgui/1.0.0/mors-gui-web/ oc start-build mors-gui-web-build --from-dir=. --follow # ConfigMap web (tuning UI) oc apply -f cm-mors-gui-web.yaml # Tworzy deployment+svc z imagestream oc new-app mors-gui-web-build:latest --name=mors-gui-web # Wstrzyknij ustawienia UI oc set env deployment/mors-gui-web --from=configmap/cm-mors-gui-web # Wstrzyknij dane DB z istniejącej configmapy (TO JEST KLUCZOWE) oc set env deployment/mors-gui-web --from=configmap/cm-mors-gui-api # Route (żeby wejść w przeglądarce) oc -n morsgui-prod create route edge mors-gui-web --service=mors-gui-web --port=8080 # Restart oc rollout restart deployment/mors-gui-web # Logi oc logs -f deployment/mors-gui-web
Po zmianach w kodzie aplikacji
cd /repo/pakiety/morsgui/1.0.0/mors-gui-web/ oc start-build mors-gui-web-build --from-dir=. --follow oc -n morsgui-prod rollout restart deployment/mors-gui-web oc -n morsgui-prod logs -f deployment/mors-gui-web
Kod aplikacji mors-gui-web
Kod aplikacji znajduje się w 1 pliku index.php
- Ten plik robi 2 rzeczy:
- Serwuje HTML (Bootstrap + Chart.js)
- Udostępnia API JSON pod
?api=proxmoxi?api=okd
- Ma:
- 2 wykresy (jeden pod drugim)
- legendę
- checkboxy pod każdym wykresem (włącz/wyłącz serię)
- zakres dat (datetime-local) + szybkie “ostatnie X godzin”
- auto-refresh (domyślnie ON) z ConfigMap
- downsampling do sensownej liczby punktów (żeby Chart.js nie umarł przy 1 dniu)
getTimestamp() - $start->getTimestamp();
if ($diff > $maxSeconds) {
$start = $end->sub(new DateInterval('PT' . $maxSeconds . 'S'));
}
return [$start, $end];
}
function pdo(): PDO {
$host = env('DBA_SERVER');
$port = env('DBA_PORT', '5432');
$db = env('DBA_NAME');
$user = env('DBA_USER');
$pass = env('DBA_PASSWORD', '');
$sslmode = env('DBA_SSLMODE', 'prefer'); // disable|allow|prefer|require|verify-ca|verify-full
if ($host === '' || $db === '' || $user === '') {
jsonOut([
'error' => 'Brak wymaganych zmiennych środowiskowych DBA_SERVER/DBA_NAME/DBA_USER (sprawdź cm-mors-gui-api i oc set env).'
], 500);
}
$dsn = "pgsql:host={$host};port={$port};dbname={$db};sslmode={$sslmode}";
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
// Wymuś sensowne formaty dat/czasu (opcjonalnie)
$pdo->exec("SET TIME ZONE 'UTC'");
return $pdo;
}
function downsampleRows(array $rows, int $maxPoints): array {
$n = count($rows);
if ($maxPoints <= 0 || $n <= $maxPoints) return $rows;
$step = (int)ceil($n / $maxPoints);
$out = [];
for ($i = 0; $i < $n; $i += $step) {
$out[] = $rows[$i];
}
return $out;
}
function toIsoUtc(string $ts): string {
// $ts może mieć mikrosekundy; i tak da się to sparsować
try {
$dt = new DateTimeImmutable($ts, new DateTimeZone('UTC'));
} catch (Throwable) {
$dt = new DateTimeImmutable('now', new DateTimeZone('UTC'));
}
return $dt->setTimezone(new DateTimeZone('UTC'))->format(DateTimeInterface::ATOM);
}
function apiProxmox(DateTimeImmutable $start, DateTimeImmutable $end, int $maxPoints): void {
$pdo = pdo();
$stmt = $pdo->prepare("
SELECT created_at, cpu_usage, cpu_temp_c, sys_load, ram_usage_prc, swap_usage_prc,
gpu_busy_prc, gpu_temp_c, nvme_temp_c
FROM proxmox
WHERE created_at BETWEEN :start AND :end
ORDER BY created_at ASC
");
$stmt->execute([
':start' => $start->format('Y-m-d H:i:sP'),
':end' => $end->format('Y-m-d H:i:sP'),
]);
$rows = $stmt->fetchAll();
$rows = downsampleRows($rows, $maxPoints);
$out = [];
foreach ($rows as $r) {
$load = [null, null, null];
if (isset($r['sys_load']) && $r['sys_load'] !== null) {
// oczekiwany format: "5.87,5.92,6.07"
$parts = array_map('trim', explode(',', (string)$r['sys_load']));
for ($i = 0; $i < 3; $i++) {
if (isset($parts[$i]) && $parts[$i] !== '') {
$load[$i] = (float)$parts[$i];
}
}
}
$out[] = [
't' => toIsoUtc((string)$r['created_at']),
'cpu_usage' => isset($r['cpu_usage']) ? (float)$r['cpu_usage'] : null,
'cpu_temp_c' => isset($r['cpu_temp_c']) ? (float)$r['cpu_temp_c'] : null,
'sys_load_1' => $load[0],
'sys_load_5' => $load[1],
'sys_load_15' => $load[2],
'ram_usage_prc' => isset($r['ram_usage_prc']) ? (float)$r['ram_usage_prc'] : null,
'swap_usage_prc' => isset($r['swap_usage_prc']) ? (float)$r['swap_usage_prc'] : null,
'gpu_busy_prc' => isset($r['gpu_busy_prc']) ? (float)$r['gpu_busy_prc'] : null,
'gpu_temp_c' => isset($r['gpu_temp_c']) ? (float)$r['gpu_temp_c'] : null,
'nvme_temp_c' => isset($r['nvme_temp_c']) ? (float)$r['nvme_temp_c'] : null,
];
}
jsonOut([
'meta' => [
'table' => 'proxmox',
'start' => $start->format(DateTimeInterface::ATOM),
'end' => $end->format(DateTimeInterface::ATOM),
'points' => count($out),
],
'rows' => $out,
]);
}
function apiOkd(DateTimeImmutable $start, DateTimeImmutable $end, int $maxPoints): void {
$pdo = pdo();
$stmt = $pdo->prepare("
SELECT created_at,
compute_1_cpu_prc, compute_2_cpu_prc, compute_3_cpu_prc,
control_1_cpu_prc, control_2_cpu_prc, control_3_cpu_prc,
compute_1_ram_prc, compute_2_ram_prc, compute_3_ram_prc,
control_1_ram_prc, control_2_ram_prc, control_3_ram_prc
FROM okd
WHERE created_at BETWEEN :start AND :end
ORDER BY created_at ASC
");
$stmt->execute([
':start' => $start->format('Y-m-d H:i:sP'),
':end' => $end->format('Y-m-d H:i:sP'),
]);
$rows = $stmt->fetchAll();
$rows = downsampleRows($rows, $maxPoints);
$out = [];
foreach ($rows as $r) {
$out[] = [
't' => toIsoUtc((string)$r['created_at']),
'compute_1_cpu_prc' => isset($r['compute_1_cpu_prc']) ? (float)$r['compute_1_cpu_prc'] : null,
'compute_2_cpu_prc' => isset($r['compute_2_cpu_prc']) ? (float)$r['compute_2_cpu_prc'] : null,
'compute_3_cpu_prc' => isset($r['compute_3_cpu_prc']) ? (float)$r['compute_3_cpu_prc'] : null,
'control_1_cpu_prc' => isset($r['control_1_cpu_prc']) ? (float)$r['control_1_cpu_prc'] : null,
'control_2_cpu_prc' => isset($r['control_2_cpu_prc']) ? (float)$r['control_2_cpu_prc'] : null,
'control_3_cpu_prc' => isset($r['control_3_cpu_prc']) ? (float)$r['control_3_cpu_prc'] : null,
'compute_1_ram_prc' => isset($r['compute_1_ram_prc']) ? (float)$r['compute_1_ram_prc'] : null,
'compute_2_ram_prc' => isset($r['compute_2_ram_prc']) ? (float)$r['compute_2_ram_prc'] : null,
'compute_3_ram_prc' => isset($r['compute_3_ram_prc']) ? (float)$r['compute_3_ram_prc'] : null,
'control_1_ram_prc' => isset($r['control_1_ram_prc']) ? (float)$r['control_1_ram_prc'] : null,
'control_2_ram_prc' => isset($r['control_2_ram_prc']) ? (float)$r['control_2_ram_prc'] : null,
'control_3_ram_prc' => isset($r['control_3_ram_prc']) ? (float)$r['control_3_ram_prc'] : null,
];
}
jsonOut([
'meta' => [
'table' => 'okd',
'start' => $start->format(DateTimeInterface::ATOM),
'end' => $end->format(DateTimeInterface::ATOM),
'points' => count($out),
],
'rows' => $out,
]);
}
/* ---------------------------
API mode
----------------------------*/
if (isset($_GET['api'])) {
$defaultHours = envInt('DEFAULT_RANGE_HOURS', 2);
$maxRangeHours = envInt('MAX_RANGE_HOURS', 72);
$maxPoints = envInt('MAX_POINTS', 1500);
$now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
$fallbackStart = $now->sub(new DateInterval('PT' . max(1, $defaultHours) . 'H'));
$start = parseDt($_GET['start'] ?? null, $fallbackStart);
$end = parseDt($_GET['end'] ?? null, $now);
[$start, $end] = clampRange($start, $end, $maxRangeHours);
$reqMaxPoints = isset($_GET['max_points']) && is_numeric($_GET['max_points']) ? (int)$_GET['max_points'] : $maxPoints;
$reqMaxPoints = max(200, min($reqMaxPoints, $maxPoints)); // dolny limit, górny limit
$api = (string)$_GET['api'];
if ($api === 'proxmox') apiProxmox($start, $end, $reqMaxPoints);
if ($api === 'okd') apiOkd($start, $end, $reqMaxPoints);
jsonOut(['error' => 'Nieznane api. Użyj: ?api=proxmox albo ?api=okd'], 400);
}
/* ---------------------------
UI mode (HTML)
----------------------------*/
$title = env('PAGE_TITLE', 'morsusl-gui • mors-gui-web');
$defaultHours = envInt('DEFAULT_RANGE_HOURS', 2);
$refreshSec = envInt('REFRESH_SECONDS', 10);
$maxPoints = envInt('MAX_POINTS', 1500);
$debug = envInt('DEBUG', 0);
?>
= htmlspecialchars($title, ENT_QUOTES, 'UTF-8') ?>
= htmlspecialchars($title, ENT_QUOTES, 'UTF-8') ?>
2 wykresy: proxmox i okd •
domyślnie ostatnie = (int)$defaultHours ?>h • auto-refresh co = (int)$refreshSec ?>s • max = (int)$maxPoints ?> punktów na serię
Proxmox
…
Serie (checkbox = pokaż/ukryj):
OKD
…
Serie (checkbox = pokaż/ukryj):
DEBUG=1 • DBA_SERVER== htmlspecialchars(env('DBA_SERVER',''), ENT_QUOTES, 'UTF-8') ?> • DBA_NAME== htmlspecialchars(env('DBA_NAME',''), ENT_QUOTES, 'UTF-8') ?>

