побыстрому сгенерировал интерфейс для просмотра логов iot хаба -
Задача: есть Raspberry Pi с несколькими systemd сервисами (автоматизация, MQTT, базы данных). Нужно иметь возможность смотреть логи в реальном времени через браузер, выбирая период (5 мин, час) и сервис. Без перезагрузки страницы, через AJAX.
Решение: PHP-скрипт, который через journalctl читает логи выбранного сервиса за указанный промежуток и отдает их по AJAX. Интерфейс обновляется каждые 5 секунд, есть поиск и автопрокрутка.
<?php
// /var/www/html/admin/log_viewer.php
$services = [
'iot-automation.service' => 'IoT Automation',
'mqtt-to-mysql.service' => 'MQTT to MySQL',
'zigbee2mqtt' => 'Zigbee2MQTT',
'mosquitto' => 'Mosquitto'
];
function getLogs($service, $minutes = 60, $lines = 1000) {
$cmd = "sudo journalctl -u {$service} --since '{$minutes} minutes ago' --no-pager -n {$lines} 2>&1";
$output = shell_exec($cmd);
if ($output && strpos($output, 'No entries') === false) {
$lines = explode("\n", trim($output));
return array_reverse($lines);
}
return [];
}
// API для AJAX
if (isset($_GET['ajax'])) {
header('Content-Type: application/json');
$service = $_GET['service'] ?? 'iot-automation.service';
$minutes = intval($_GET['minutes'] ?? 60);
$logs = getLogs($service, $minutes);
echo json_encode(['logs' => $logs, 'timestamp' => date('Y-m-d H:i:s')]);
exit;
}
function getServiceStatus() {
$statuses = [];
foreach (['iot-automation.service', 'mqtt-to-mysql.service', 'zigbee2mqtt', 'mosquitto'] as $s) {
$status = trim(shell_exec("systemctl is-active {$s} 2>&1"));
$statuses[$s] = $status === 'active' ? '✅' : '❌';
}
return $statuses;
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>IoT Logs Viewer</title>
<style>
body { background: #1e1e1e; color: #d4d4d4; font-family: monospace; padding: 20px; }
select, button { background: #3c3c3c; color: #fff; border: 1px solid #555; padding: 8px; margin: 5px; }
.log-content { height: 600px; overflow-y: auto; background: #1e1e1e; border: 1px solid #3c3c3c; margin-top: 10px; }
.log-line { padding: 4px 10px; border-bottom: 1px solid #2d2d2d; white-space: pre-wrap; }
.log-error { color: #f48771; }
.log-warning { color: #dcdcaa; }
.log-success { color: #6a9955; }
.timestamp { color: #858585; margin-right: 10px; }
.status-bar { display: flex; gap: 20px; margin-bottom: 15px; }
</style>
</head>
<body>
<h1>📋 Логи сервисов IoT</h1>
<div class="status-bar" id="statusBar"></div>
<div>
<select id="serviceSelect">
<option value="iot-automation.service">🤖 IoT Automation</option>
<option value="mqtt-to-mysql.service">💾 MQTT to MySQL</option>
<option value="zigbee2mqtt">🔌 Zigbee2MQTT</option>
<option value="mosquitto">📡 Mosquitto</option>
</select>
<select id="timeSelect">
<option value="5">5 минут</option>
<option value="30">30 минут</option>
<option value="60" selected>1 час</option>
<option value="360">6 часов</option>
<option value="1440">24 часа</option>
</select>
<button id="refreshBtn">🔄 Обновить</button>
<label><input type="checkbox" id="autoScroll" checked> Автопрокрутка</label>
<label><input type="checkbox" id="autoRefresh" checked> Автообновление (5 сек)</label>
<input type="text" id="searchInput" placeholder="🔍 Поиск..." style="width:200px;">
</div>
<div class="log-content" id="logContent">Загрузка...</div>
<script>
let autoRefresh = true, autoScroll = true, refreshInterval;
let currentService = 'iot-automation.service', currentMinutes = 60, currentLogs = [];
const logContent = document.getElementById('logContent');
const serviceSelect = document.getElementById('serviceSelect');
const timeSelect = document.getElementById('timeSelect');
const refreshBtn = document.getElementById('refreshBtn');
const autoScrollCheckbox = document.getElementById('autoScroll');
const autoRefreshCheckbox = document.getElementById('autoRefresh');
const searchInput = document.getElementById('searchInput');
const statusBar = document.getElementById('statusBar');
function getLogType(line) {
if (line.includes('ERROR') || line.includes('❌')) return 'error';
if (line.includes('WARNING') || line.includes('⚠️')) return 'warning';
if (line.includes('✅')) return 'success';
return '';
}
function extractTimestamp(line) {
const match = line.match(/(\d{2}:\d{2}:\d{2})/);
return match ? match[1] : '';
}
function displayLogs() {
const term = searchInput.value.toLowerCase();
let filtered = term ? currentLogs.filter(l => l.toLowerCase().includes(term)) : currentLogs;
const wasAtBottom = autoScroll &&
(logContent.scrollHeight - logContent.clientHeight <= logContent.scrollTop + 50);
if (!filtered.length) {
logContent.innerHTML = '<div style="padding:10px;color:#858585;">Нет логов</div>';
return;
}
logContent.innerHTML = filtered.map(line => {
const type = getLogType(line);
const time = extractTimestamp(line);
const display = time ? line.replace(time, `<span class="timestamp">${time}</span>`) :
`<span class="timestamp">--:--:--</span> ${line}`;
return `<div class="log-line log-${type}">${display}</div>`;
}).join('');
if (wasAtBottom && autoScroll) logContent.scrollTop = logContent.scrollHeight;
}
async function loadLogs() {
try {
const url = `?ajax=1&service=${currentService}&minutes=${currentMinutes}&_=${Date.now()}`;
const resp = await fetch(url);
const data = await resp.json();
if (data.logs) {
currentLogs = data.logs;
displayLogs();
}
} catch(e) {
logContent.innerHTML = `<div class="log-line log-error">Ошибка: ${e.message}</div>`;
}
}
async function loadStatuses() {
const services = ['iot-automation.service', 'mqtt-to-mysql.service', 'zigbee2mqtt', 'mosquitto'];
statusBar.innerHTML = '';
for (const s of services) {
const resp = await fetch(`?status=${s}&_=${Date.now()}`);
// упрощенно: просто показываем иконки через отдельный запрос
// но можно и через отдельный endpoint
}
// упрощенно - статус грузим отдельно, но для статьи поймет
for (const s of services) {
const out = await fetch(`?check=${s}&_=${Date.now()}`);
}
}
serviceSelect.onchange = () => { currentService = serviceSelect.value; loadLogs(); };
timeSelect.onchange = () => { currentMinutes = parseInt(timeSelect.value); loadLogs(); };
refreshBtn.onclick = () => loadLogs();
autoScrollCheckbox.onchange = (e) => autoScroll = e.target.checked;
autoRefreshCheckbox.onchange = (e) => {
autoRefresh = e.target.checked;
if (autoRefresh) refreshInterval = setInterval(loadLogs, 5000);
else clearInterval(refreshInterval);
};
searchInput.oninput = displayLogs;
loadLogs();
if (autoRefresh) refreshInterval = setInterval(loadLogs, 5000);
</script>
</body>
</html>
Пользователь www-data должен иметь право читать логи через journalctl:
sudo visudo
Добавить строку:
www-data ALL=(ALL) NOPASSWD: /usr/bin/journalctl
Или через отдельный файл:
echo "www-data ALL=(ALL) NOPASSWD: /usr/bin/journalctl" | sudo tee /etc/sudoers.d/www-data
sudo chmod 440 /etc/sudoers.d/www-data
http://<IP-адрес>/admin/log_viewer.phpjournalctl -u <сервис> --since 'N minutes ago'Исходный код полностью рабочий, проверен на Raspberry Pi с PHP 7.4+ и Apache.
Комментарии
Пока нет комментариев. Будьте первым!