Auto-load only new log data

This is way more complicated that just reloading the log file, but also
way nicer.

Signed-off-by: Pablo Zmdl <pablo@nextcloud.com>
This commit is contained in:
Pablo Zmdl
2026-02-24 12:32:54 +01:00
parent 0633f665d5
commit 680a2fefcb
7 changed files with 261 additions and 55 deletions

View File

@@ -11,6 +11,7 @@ ini_set('max_execution_time', '7200');
ini_set('log_errors_max_len', '0');
use DI\Container;
use DI\NotFoundException;
use Slim\Csrf\Guard;
use Slim\Factory\AppFactory;
use Slim\Views\Twig;
@@ -171,6 +172,15 @@ $app->get('/setup', function (Request $request, Response $response, array $args)
]
);
});
$app->get('/log', function (Request $request, Response $response, array $args) use ($container) {
$params = $request->getQueryParams();
$id = $params['id'] ?? '';
if (!str_starts_with($id, 'nextcloud-aio-')) {
throw new DI\NotFoundException();
}
$view = Twig::fromRequest($request);
return $view->render($response, 'log.twig', ['id' => $id]);
});
// Auth Redirector
$app->get('/', function (\Psr\Http\Message\RequestInterface $request, Response $response, array $args) use ($container) {

142
php/public/log-view.js Normal file
View File

@@ -0,0 +1,142 @@
class LogViewer {
// Configure the interval in seconds for autoloading log data.
autoloadIntervalSec = 5;
// Set to true to see some debug log statements in the browser console.
debugLog = false;
// Don't touch these, please.
containerId;
apiBaseUrl = 'api/docker/logs';
autoloadIntervalId = null;
logElem;
lastLogTimestamp = '';
autoloadingDisabledFromButton = false;
loaderElem;
dataLoadingLock;
constructor() {
const id = document.body.dataset.containerId;
if (typeof(id) !== 'string' || !id.startsWith('nextcloud-aio-')) {
throw new Exception('Invalid container ID');
}
this.containerId = id;
this.logElem = document.querySelector('pre');
this.loaderElem = document.querySelector('.loader');
this.initAutoloadingControls();
// Enable automatic log data loading.
this.startAutoloading();
}
startAutoloading() {
// Load log data immediately.
this.loadAndAppendLogData();
// Load new log data repeatedly.
this.debug("Starting autoloading");
this.autoloadIntervalId = setInterval(() => {
if (this.isAutoloadingEnabled()) {
this.loadAndAppendLogData();
}
}, 5000);
}
stopAutoloading() {
this.debug("Stopping autoloading");
clearInterval(this.autoloadIntervalId);
this.autoloadIntervalId = null;
}
isAutoloadingEnabled() {
return !!this.autoloadIntervalId;
}
getUrl() {
return `${this.apiBaseUrl}?id=${this.containerId}&since=${this.lastLogTimestamp}`;
}
debug(...args) {
if (this.debugLog) {
console.debug('LogViewer:', ...args);
}
}
// Load log data and append it to the DOM.
loadAndAppendLogData() {
if (this.dataLoadingLock) {
this.debug("Another log data loading request is still running, cancelling this request");
return;
}
this.debug("Loading new log data");
this.dataLoadingLock = true;
this.loaderElem.classList.remove('hidden');
fetch(this.getUrl())
.then((response) => {
if (!response.ok) {
throw new Error("Error while fetching log data!");
}
return response;
})
.then((response) => response.text())
.then((text) => {
text = text.trim();
if (text.length === 0) {
this.debug("Received no new log data from server");
return;
}
this.debug("Received", Math.round(text.length / 1024), "KB of new log data from server");
this.logElem.append(text + "\n");
this.scrollToBottom();
this.lastLogTimestamp = text.split("\n").at(-1)?.split(' ')[0] ?? '';
})
.finally(() => {
this.dataLoadingLock = false;
this.loaderElem.classList.add('hidden');
this.debug("Finished log data loading");
})
.catch((err) => console.error(err));
}
scrollToBottom() {
window.scrollTo(0, document.body.scrollHeight);
}
initAutoloadingControls() {
// Provide a button that allows to manually disable the autoloading.
const button = document.getElementById('autoloading-control');
const statusElem = document.getElementById('autoloading-status');
if (!button) {
return;
}
button.addEventListener('click', (event) => {
event.preventDefault();
if (this.isAutoloadingEnabled()) {
this.stopAutoloading();
statusElem.textContent = 'disabled';
button.textContent = 'Enable';
this.autoloadingDisabledFromButton = true;
} else {
this.startAutoloading();
statusElem.textContent = 'enabled';
button.textContent = 'Disable';
this.autoloadingDisabledFromButton = false;
}
});
// Load new data immediately if the window gets visible to the user again (unless autoloading has been
// disabled).
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
this.debug("Window became visible");
if (!this.autoloadingDisabledFromButton) {
this.startAutoloading();
}
} else {
this.debug("Window became hidden");
this.stopAutoloading();
}
});
}
}
document.addEventListener("DOMContentLoaded", () => {
new LogViewer();
});