potential fix for logging on server
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@ -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
|
||||||
|
|||||||
112
lib/api_log.php
112
lib/api_log.php
@ -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
0
uploads/logs/.gitkeep
Normal file
0
uploads/logs/api/.gitkeep
Normal file
0
uploads/logs/api/.gitkeep
Normal 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)' : '';
|
||||||
|
|||||||
Reference in New Issue
Block a user