/
gesar93
/
gesar.chlogs
Обзор
Документация
Войти
/
gesar93
/
gesar.chlogs
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
admin/offset.php
386 строк
18 KB
Ilya Donskikh
Initial
02 фев 2026, 09:43
02 фев 2026, 09:43
e45c3ed
Код
Авторство
О чём код?
<?php // Kafka Consumer Offset Manager // Allows viewing and resetting the Kafka consumer offset use Bitrix\Gesar\Kafka\Util\Paths; use Bitrix\Main\Loader; require_once $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_admin_before.php'; global $USER, $APPLICATION; if (!$USER || !$USER->IsAdmin()) { require_once $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_admin_after.php'; echo 'Access denied'; require_once $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/epilog_admin.php'; die(); } Loader::includeModule('gesar.chlogs'); if (!Loader::includeModule('gesar.kafka')) { require_once $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_admin_after.php'; echo 'Module gesar.kafka is not installed.'; require_once $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/epilog_admin.php'; die(); } // Path to offset file $offsetFilePath = Paths::varPath('offset/kafka_offset.txt'); Paths::ensureDir(dirname($offsetFilePath)); $response = ['success' => false, 'message' => '', 'offset' => null]; // Handle AJAX requests if ($_SERVER['REQUEST_METHOD'] === 'POST') { header('Content-Type: application/json; charset=utf-8'); try { $action = $_POST['action'] ?? ''; if ($action === 'get_offset') { if (file_exists($offsetFilePath)) { $offset = trim(file_get_contents($offsetFilePath)); $response['success'] = true; $response['offset'] = (int)$offset; $response['message'] = 'Текущий offset: ' . $offset; } else { $response['success'] = true; $response['offset'] = null; $response['message'] = 'Файл offset не найден (консьюмер еще не запускался)'; } } elseif ($action === 'set_offset') { $newOffset = trim($_POST['offset'] ?? ''); if (!is_numeric($newOffset) || (int)$newOffset < 0) { throw new \Exception('Некорректное значение offset: должно быть неотрицательное число'); } $newOffset = (int)$newOffset; // Backup current offset if (file_exists($offsetFilePath)) { $currentOffset = trim(file_get_contents($offsetFilePath)); $backupPath = $offsetFilePath . '.backup.' . date('Y-m-d_His'); file_put_contents($backupPath, $currentOffset); error_log("Kafka offset backed up: {$currentOffset} -> {$backupPath}"); } // Set new offset file_put_contents($offsetFilePath, $newOffset); $response['success'] = true; $response['offset'] = $newOffset; $response['message'] = "Offset установлен: {$newOffset}. Изменения применятся автоматически на следующей итерации Consumer."; error_log("Kafka offset manually set to: {$newOffset} by admin user ID: " . $USER->GetID()); } elseif ($action === 'backup_offset') { if (!file_exists($offsetFilePath)) { throw new \Exception('Файл offset не существует'); } $currentOffset = trim(file_get_contents($offsetFilePath)); $backupPath = $offsetFilePath . '.backup.' . date('Y-m-d_His'); file_put_contents($backupPath, $currentOffset); $response['success'] = true; $response['message'] = "Создана резервная копия offset: {$currentOffset}"; } elseif ($action === 'list_backups') { $backupFiles = glob($offsetFilePath . '.backup.*'); $backups = []; foreach ($backupFiles as $file) { $backups[] = [ 'filename' => basename($file), 'offset' => trim(file_get_contents($file)), 'created' => date('Y-m-d H:i:s', filemtime($file)), 'size' => filesize($file) ]; } usort($backups, function($a, $b) { return strcmp($b['created'], $a['created']); }); $response['success'] = true; $response['backups'] = $backups; } else { throw new \Exception('Неизвестное действие: ' . $action); } } catch (\Exception $e) { $response['success'] = false; $response['message'] = $e->getMessage(); error_log('Offset manager error: ' . $e->getMessage()); } echo json_encode($response, JSON_UNESCAPED_UNICODE); exit; } // Get current offset for display $currentOffset = null; if (file_exists($offsetFilePath)) { $currentOffset = trim(file_get_contents($offsetFilePath)); } $APPLICATION->SetTitle('Kafka Offset'); require_once $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_admin_after.php'; ?> <style> .gesar-chlogs-offset, .gesar-chlogs-offset * { box-sizing: border-box; } .gesar-chlogs-offset { font-family: system-ui, -apple-system, sans-serif; padding: 20px; background: #f8fafc; color: #1e293b; } .gesar-chlogs-offset .container { max-width: 900px; margin: 0 auto; } .gesar-chlogs-offset h1 { margin-bottom: 24px; color: #0f172a; font-size: 28px; } .gesar-chlogs-offset .card { background: #fff; border-radius: 8px; padding: 24px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } .gesar-chlogs-offset .card h2 { margin-bottom: 16px; font-size: 20px; color: #334155; } .gesar-chlogs-offset .info-box { background: #f1f5f9; border-left: 4px solid #3b82f6; padding: 16px; border-radius: 6px; margin-bottom: 16px; } .gesar-chlogs-offset .info-box strong { color: #1e293b; } .gesar-chlogs-offset .warning-box { background: #fef3c7; border-left: 4px solid #f59e0b; padding: 16px; border-radius: 6px; margin-bottom: 16px; } .gesar-chlogs-offset .form-group { margin-bottom: 16px; } .gesar-chlogs-offset .form-group label { display: block; margin-bottom: 6px; font-weight: 500; color: #475569; } .gesar-chlogs-offset .form-group input { width: 100%; max-width: 300px; padding: 10px 12px; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 14px; font-family: monospace; } .gesar-chlogs-offset .btn { padding: 10px 20px; background: #3b82f6; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500; margin-right: 8px; } .gesar-chlogs-offset .btn:hover { background: #2563eb; } .gesar-chlogs-offset .btn:disabled { background: #94a3b8; cursor: not-allowed; } .gesar-chlogs-offset .btn-danger { background: #ef4444; } .gesar-chlogs-offset .btn-danger:hover { background: #dc2626; } .gesar-chlogs-offset .btn-secondary { background: #64748b; } .gesar-chlogs-offset .btn-secondary:hover { background: #475569; } .gesar-chlogs-offset .alert { padding: 12px 16px; border-radius: 6px; margin-bottom: 16px; } .gesar-chlogs-offset .alert-success { background: #d1fae5; border: 1px solid #6ee7b7; color: #065f46; } .gesar-chlogs-offset .alert-error { background: #fee2e2; border: 1px solid #fca5a5; color: #991b1b; } .gesar-chlogs-offset .back-link { display: inline-block; margin-bottom: 16px; color: #3b82f6; text-decoration: none; } .gesar-chlogs-offset .back-link:hover { text-decoration: underline; } .gesar-chlogs-offset .help-text { font-size: 13px; color: #64748b; margin-top: 4px; } .gesar-chlogs-offset .current-offset { font-size: 32px; font-weight: bold; color: #3b82f6; font-family: monospace; } .gesar-chlogs-offset .backups-table { width: 100%; border-collapse: collapse; margin-top: 12px; } .gesar-chlogs-offset .backups-table th, .gesar-chlogs-offset .backups-table td { padding: 10px; text-align: left; border-bottom: 1px solid #e5e7eb; } .gesar-chlogs-offset .backups-table th { background: #f8fafc; font-weight: 600; } .gesar-chlogs-offset .backups-table td { font-family: monospace; font-size: 13px; } .spinner { display: none; border: 3px solid #f3f4f6; border-top: 3px solid #3b82f6; border-radius: 50%; width: 16px; height: 16px; animation: spin 1s linear infinite; margin-left: 8px; display: inline-block; vertical-align: middle; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style> <div class="gesar-chlogs-offset"> <div class="container"> <a href="gesar_chlogs_viewer.php" class="back-link">← Вернуться к логам</a> <h1>⚙️ Управление Kafka Consumer Offset</h1> <div class="warning-box"> <strong>⚠️ Внимание!</strong> Изменение offset влияет на обработку сообщений. <ul style="margin-top: 8px; margin-left: 20px;"> <li>Установка меньшего значения приведет к повторной обработке сообщений</li> <li>Установка большего значения пропустит необработанные сообщения</li> <li>✨ Изменения применяются автоматически на следующей итерации Consumer (перезапуск не требуется)</li> </ul> </div> <!-- Current Offset --> <div class="card"> <h2>Текущий Offset</h2> <div id="offset-info"></div> <div style="margin-top: 16px;"> <button onclick="refreshOffset()" class="btn btn-secondary"> 🔄 Обновить <span class="spinner" id="refresh-spinner" style="display: none;"></span> </button> <button onclick="backupOffset()" class="btn btn-secondary"> 💾 Создать резервную копию </button> </div> </div> <!-- Set Offset --> <div class="card"> <h2>Установить новый Offset</h2> <div id="set-result"></div> <form id="set-offset-form" style="margin-top: 16px;"> <div class="form-group"> <label for="new_offset">Новое значение offset:</label> <input type="number" id="new_offset" name="offset" min="0" placeholder="87126" required> <div class="help-text">Укажите номер offset, с которого начать чтение (0 = начало темы)</div> </div> <button type="submit" class="btn btn-danger"> ⚡ Установить offset <span class="spinner" id="set-spinner" style="display: none;"></span> </button> </form> </div> <!-- Backups --> <div class="card"> <h2>Резервные копии</h2> <p class="help-text" style="margin-bottom: 12px;">Автоматически создаются при изменении offset</p> <div id="backups-list">Загрузка...</div> <button onclick="loadBackups()" class="btn btn-secondary" style="margin-top: 12px;"> 🔄 Обновить список </button> </div> </div> <script> // Load offset on page load window.addEventListener('load', () => { refreshOffset(); loadBackups(); }); async function refreshOffset() { const infoDiv = document.getElementById('offset-info'); const spinner = document.getElementById('refresh-spinner'); spinner.style.display = 'inline-block'; try { const response = await fetch(location.pathname, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'action=get_offset' }); const data = await response.json(); if (data.success) { if (data.offset !== null) { infoDiv.innerHTML = ` <div class="info-box"> <div style="margin-bottom: 8px;">Последний обработанный offset:</div> <div class="current-offset">${data.offset.toLocaleString()}</div> </div> `; } else { infoDiv.innerHTML = ` <div class="info-box"> <strong>ℹ️ ${data.message}</strong> </div> `; } } else { infoDiv.innerHTML = `<div class="alert alert-error">❌ ${data.message}</div>`; } } catch (error) { infoDiv.innerHTML = `<div class="alert alert-error">❌ Ошибка: ${error.message}</div>`; } finally { spinner.style.display = 'none'; } } async function backupOffset() { if (!confirm('Создать резервную копию текущего offset?')) { return; } try { const response = await fetch(location.pathname, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'action=backup_offset' }); const data = await response.json(); if (data.success) { alert('✅ ' + data.message); loadBackups(); } else { alert('❌ ' + data.message); } } catch (error) { alert('❌ Ошибка: ' + error.message); } } document.getElementById('set-offset-form').addEventListener('submit', async (e) => { e.preventDefault(); const newOffset = document.getElementById('new_offset').value; if (!confirm(`Установить offset в значение ${newOffset}?\n\n✨ Consumer автоматически перезапустится с нового offset на следующей итерации.`)) { return; } const resultDiv = document.getElementById('set-result'); const spinner = document.getElementById('set-spinner'); const btn = e.target.querySelector('button'); btn.disabled = true; spinner.style.display = 'inline-block'; resultDiv.innerHTML = ''; try { const response = await fetch(location.pathname, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: `action=set_offset&offset=${encodeURIComponent(newOffset)}` }); const data = await response.json(); if (data.success) { resultDiv.innerHTML = `<div class="alert alert-success">✅ ${data.message}</div>`; refreshOffset(); loadBackups(); document.getElementById('new_offset').value = ''; } else { resultDiv.innerHTML = `<div class="alert alert-error">❌ ${data.message}</div>`; } } catch (error) { resultDiv.innerHTML = `<div class="alert alert-error">❌ Ошибка: ${error.message}</div>`; } finally { btn.disabled = false; spinner.style.display = 'none'; } }); async function loadBackups() { const listDiv = document.getElementById('backups-list'); listDiv.innerHTML = 'Загрузка...'; try { const response = await fetch(location.pathname, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'action=list_backups' }); const data = await response.json(); if (data.success && data.backups && data.backups.length > 0) { let html = '<table class="backups-table"><thead><tr>'; html += '<th>Дата создания</th><th>Offset</th><th>Файл</th>'; html += '</tr></thead><tbody>'; data.backups.forEach(backup => { html += '<tr>'; html += `<td>${backup.created}</td>`; html += `<td>${parseInt(backup.offset).toLocaleString()}</td>`; html += `<td>${backup.filename}</td>`; html += '</tr>'; }); html += '</tbody></table>'; listDiv.innerHTML = html; } else { listDiv.innerHTML = '<div class="help-text">Резервные копии не найдены</div>'; } } catch (error) { listDiv.innerHTML = `<div class="alert alert-error">Ошибка загрузки: ${error.message}</div>`; } } </script> </div> <?php require_once $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/epilog_admin.php'; ?>