potential fix for logging on server

This commit is contained in:
tom.hempel
2026-06-01 22:50:22 +02:00
parent e5ac069dc4
commit e4d55f4c90
5 changed files with 138 additions and 6 deletions

2
.gitignore vendored
View File

@ -7,6 +7,8 @@ valid_tokens.txt
# Encrypted database, temp files, backups # Encrypted database, temp files, backups
uploads/* uploads/*
!uploads/.gitkeep !uploads/.gitkeep
!uploads/logs/
!uploads/logs/**
# Database lock files (just in case) # Database lock files (just in case)
uploads/.qdb_lock uploads/.qdb_lock

View File

@ -10,7 +10,8 @@
* - web_change: POST/PUT/PATCH/DELETE from the management website (incl. translation labels) * - web_change: POST/PUT/PATCH/DELETE from the management website (incl. translation labels)
* - web_export: Website downloads (CSV/ZIP/JSON exports, translation bundle) * - web_export: Website downloads (CSV/ZIP/JSON exports, translation bundle)
* *
* Set QDB_API_LOG=0 to disable. Logs live under logs/api/ (gitignored). * Set QDB_API_LOG=0 to disable. Set QDB_API_LOG_DIR for a custom path.
* Default directory is uploads/logs/api (writable by PHP alongside the database).
*/ */
/** @var list<string> */ /** @var list<string> */
@ -28,11 +29,98 @@ function qdb_api_log_enabled(): bool
function qdb_api_log_dir(): string function qdb_api_log_dir(): string
{ {
static $resolved = null;
if ($resolved !== null) {
return $resolved;
}
$custom = qdb_env_get('QDB_API_LOG_DIR'); $custom = qdb_env_get('QDB_API_LOG_DIR');
if ($custom !== null && $custom !== '') { if ($custom !== null && $custom !== '') {
return rtrim($custom, '/\\'); $resolved = rtrim($custom, '/\\');
return $resolved;
} }
return dirname(__DIR__) . '/logs/api';
$root = dirname(__DIR__);
foreach ([$root . '/uploads/logs/api', $root . '/logs/api'] as $candidate) {
if (qdb_api_log_dir_is_usable($candidate)) {
$resolved = $candidate;
return $resolved;
}
}
$resolved = $root . '/uploads/logs/api';
return $resolved;
}
function qdb_api_log_dir_is_usable(string $dir): bool
{
if (is_dir($dir)) {
return is_writable($dir);
}
$parent = dirname($dir);
if (is_dir($parent) && is_writable($parent)) {
return true;
}
if (@mkdir($dir, 0775, true)) {
return is_writable($dir);
}
return is_dir($dir) && is_writable($dir);
}
/**
* Diagnostics for admin UI (permissions, path, enabled flag).
*
* @return array<string, mixed>
*/
function qdb_api_log_status(): array
{
$dir = qdb_api_log_dir();
$enabled = qdb_api_log_enabled();
$exists = is_dir($dir);
$writable = false;
$probeError = null;
if ($enabled) {
if (!$exists) {
if (!@mkdir($dir, 0775, true)) {
$probeError = 'Cannot create directory: ' . $dir;
}
$exists = is_dir($dir);
}
if ($exists && !is_writable($dir)) {
$probeError = 'Directory exists but is not writable by PHP: ' . $dir;
} elseif ($exists) {
$probe = $dir . '/.write_probe';
if (@file_put_contents($probe, (string)time() . "\n", LOCK_EX) === false) {
$probeError = 'Cannot write files in: ' . $dir;
} else {
$writable = true;
@unlink($probe);
}
}
}
$phpUser = null;
if (function_exists('posix_geteuid') && function_exists('posix_getpwuid')) {
$info = posix_getpwuid(posix_geteuid());
if (is_array($info) && !empty($info['name'])) {
$phpUser = (string)$info['name'];
}
}
return [
'enabled' => $enabled,
'dir' => $dir,
'exists' => $exists,
'writable' => $writable,
'error' => $probeError,
'php_user' => $phpUser,
'fix_hint' => $writable || !$enabled
? null
: 'On the server run: mkdir -p ' . $dir . ' && chown -R www-data:www-data '
. dirname($dir) . ' && chmod -R 775 ' . dirname($dir)
. ' (adjust www-data to your PHP user). Or set QDB_API_LOG_DIR in .env to a writable path.',
];
} }
function qdb_api_log_max_bytes(): int function qdb_api_log_max_bytes(): int
@ -421,13 +509,24 @@ function qdb_api_log_change_scalar(mixed $v): string|int|float|bool|null
function qdb_api_log_append_line(string $line): void function qdb_api_log_append_line(string $line): void
{ {
$dir = qdb_api_log_dir(); $dir = qdb_api_log_dir();
if (!is_dir($dir) && !@mkdir($dir, 0755, true) && !is_dir($dir)) { if (!is_dir($dir) && !@mkdir($dir, 0775, true) && !is_dir($dir)) {
throw new RuntimeException("Cannot create log directory: $dir"); $err = error_get_last();
throw new RuntimeException(
'Cannot create log directory: ' . $dir
. ($err ? ' (' . ($err['message'] ?? '') . ')' : '')
);
}
if (!is_writable($dir)) {
throw new RuntimeException('Log directory not writable: ' . $dir);
} }
$path = qdb_api_log_resolve_write_path($dir); $path = qdb_api_log_resolve_write_path($dir);
if (@file_put_contents($path, $line, FILE_APPEND | LOCK_EX) === false) { if (@file_put_contents($path, $line, FILE_APPEND | LOCK_EX) === false) {
throw new RuntimeException("Cannot write API log: $path"); $err = error_get_last();
throw new RuntimeException(
'Cannot write API log: ' . $path
. ($err ? ' (' . ($err['message'] ?? '') . ')' : '')
);
} }
} }
@ -507,6 +606,7 @@ function qdb_api_log_read_entries(string $date, string $activityFilter = '', int
'files' => array_map('basename', $files), 'files' => array_map('basename', $files),
'truncated' => $truncated, 'truncated' => $truncated,
'kinds' => QDB_API_LOG_ACTIVITIES, 'kinds' => QDB_API_LOG_ACTIVITIES,
'logStatus' => qdb_api_log_status(),
]; ];
} }

0
uploads/logs/.gitkeep Normal file
View File

View File

View File

@ -78,6 +78,7 @@ export function devPage() {
</select> </select>
<button type="button" class="btn btn-sm" id="activityLogRefresh">Refresh</button> <button type="button" class="btn btn-sm" id="activityLogRefresh">Refresh</button>
</div> </div>
<div id="activityLogStatus" class="field-hint" style="margin:0 0 10px;display:none"></div>
<p id="activityLogMeta" class="field-hint" style="margin:0 0 10px"></p> <p id="activityLogMeta" class="field-hint" style="margin:0 0 10px"></p>
<div class="table-wrapper"> <div class="table-wrapper">
<table class="data-table activity-log-table"> <table class="data-table activity-log-table">
@ -250,6 +251,33 @@ export function devPage() {
}); });
} }
function renderActivityLogStatus(status) {
const el = document.getElementById('activityLogStatus');
if (!el || !status) {
if (el) el.style.display = 'none';
return;
}
if (!status.enabled) {
el.className = 'field-hint callout-warn';
el.innerHTML = 'Activity logging is disabled (<code>QDB_API_LOG=0</code> in .env).';
el.style.display = '';
return;
}
if (status.writable) {
el.className = 'field-hint';
el.innerHTML = `Logging to <code>${escHtml(status.dir)}</code>`
+ (status.php_user ? ` (PHP user: <code>${escHtml(status.php_user)}</code>)` : '');
el.style.display = '';
return;
}
el.className = 'field-hint callout-danger';
el.innerHTML = `<strong>Cannot write activity log.</strong> ${escHtml(status.error || 'Permission problem.')}
<br>Path: <code>${escHtml(status.dir)}</code>`
+ (status.php_user ? `<br>PHP runs as: <code>${escHtml(status.php_user)}</code>` : '')
+ (status.fix_hint ? `<br>${escHtml(status.fix_hint)}` : '');
el.style.display = '';
}
function wireActivityLog() { function wireActivityLog() {
const dateEl = document.getElementById('activityLogDate'); const dateEl = document.getElementById('activityLogDate');
const kindEl = document.getElementById('activityLogKind'); const kindEl = document.getElementById('activityLogKind');
@ -274,6 +302,8 @@ async function loadActivityLog() {
const data = await apiGet(`activity-log?${q}`); const data = await apiGet(`activity-log?${q}`);
if (!data.success) throw new Error(data.error || 'Failed to load activity log'); if (!data.success) throw new Error(data.error || 'Failed to load activity log');
renderActivityLogStatus(data.logStatus);
const entries = data.entries || []; const entries = data.entries || [];
const files = (data.files || []).join(', ') || 'none'; const files = (data.files || []).join(', ') || 'none';
const trunc = data.truncated ? ' (newest 300 shown)' : ''; const trunc = data.truncated ? ' (newest 300 shown)' : '';