Refactor error handling and logging in API responses

This commit is contained in:
2026-06-03 17:16:14 +02:00
parent 30d3ab4a87
commit d0daa7e937
27 changed files with 808 additions and 220 deletions

View File

@ -15,7 +15,7 @@
*/
/** @var list<string> */
const QDB_API_LOG_ACTIVITIES = ['app_sync', 'app_change', 'web_change', 'web_export'];
const QDB_API_LOG_ACTIVITIES = ['app_sync', 'app_change', 'web_change', 'web_export', 'api_error'];
function qdb_api_log_enabled(): bool
{
@ -150,7 +150,7 @@ function qdb_api_log_classify_activity(string $method, string $client, string $r
$client = strtolower(trim($client));
$route = strtolower(trim($route));
if ($method === 'OPTIONS' || in_array($route, ['health', 'activity-log', 'session'], true)) {
if ($method === 'OPTIONS' || in_array($route, ['activity-log', 'session'], true)) {
return null;
}
@ -191,37 +191,48 @@ function qdb_api_log_begin(string $method, string $route, array $context = []):
$client = trim((string)($_SERVER['HTTP_X_QDB_CLIENT'] ?? ''));
$activity = qdb_api_log_classify_activity($method, $client, $route);
if ($activity === null) {
$GLOBALS['qdb_api_log_ctx'] = ['skip' => true];
return;
}
$GLOBALS['qdb_api_log_ctx'] = array_merge([
'started_at' => microtime(true),
'method' => strtoupper($method),
'route' => $route,
'activity' => $activity,
'uri' => (string)($_SERVER['REQUEST_URI'] ?? ''),
'ip' => qdb_api_log_client_ip(),
'client' => $client,
'user_agent' => trim((string)($_SERVER['HTTP_USER_AGENT'] ?? '')),
], $context);
if ($activity !== null) {
$GLOBALS['qdb_api_log_ctx']['activity'] = $activity;
} else {
$GLOBALS['qdb_api_log_ctx']['skip_success'] = true;
}
}
function qdb_api_log_note_exception(Throwable $e): void
{
if (empty($GLOBALS['qdb_api_log_ctx']) || !empty($GLOBALS['qdb_api_log_ctx']['skip'])) {
if (empty($GLOBALS['qdb_api_log_ctx'])) {
return;
}
$GLOBALS['qdb_api_log_ctx']['exception'] = $e->getMessage();
$GLOBALS['qdb_api_log_ctx']['exception_class'] = $e::class;
}
function qdb_api_log_note_api_error(string $code, string $message, int $status): void
{
if (empty($GLOBALS['qdb_api_log_ctx'])) {
return;
}
$GLOBALS['qdb_api_log_ctx']['api_error_code'] = $code;
$GLOBALS['qdb_api_log_ctx']['api_error_message'] = $message;
$GLOBALS['qdb_api_log_ctx']['api_error_status'] = $status;
}
function qdb_api_log_finish(): void
{
$ctx = $GLOBALS['qdb_api_log_ctx'] ?? null;
unset($GLOBALS['qdb_api_log_ctx']);
if (!is_array($ctx) || !qdb_api_log_enabled() || !empty($ctx['skip'])) {
if (!is_array($ctx) || !qdb_api_log_enabled()) {
return;
}
@ -231,6 +242,14 @@ function qdb_api_log_finish(): void
$status = 200;
}
$isError = $status >= 400
|| !empty($ctx['exception'])
|| !empty($ctx['api_error_code']);
if (!empty($ctx['skip_success']) && !$isError) {
return;
}
$token = get_bearer_token();
$auth = ['role' => null, 'userID' => null, 'token_hint' => null];
if ($token !== null && $token !== '') {
@ -256,9 +275,14 @@ function qdb_api_log_finish(): void
$actorUsername = qdb_api_log_lookup_username($auth['userID'] ?? null);
$targetUsername = qdb_api_log_extract_target_username($bodyLog, $queryLog);
$activity = $ctx['activity'] ?? null;
if ($isError) {
$activity = 'api_error';
}
$entry = [
'ts' => gmdate('c'),
'activity' => $ctx['activity'] ?? null,
'activity' => $activity,
'method' => $ctx['method'] ?? '',
'route' => $ctx['route'] ?? '',
'status' => $status,
@ -281,11 +305,19 @@ function qdb_api_log_finish(): void
),
];
if (!empty($ctx['api_error_code'])) {
$entry['error_code'] = (string)$ctx['api_error_code'];
$entry['error_message'] = (string)($ctx['api_error_message'] ?? '');
$entry['error'] = $entry['error_code'] . ': ' . $entry['error_message'];
}
if (!empty($ctx['exception'])) {
$entry['error'] = (string)$ctx['exception'];
$entry['error_internal'] = (string)$ctx['exception'];
if (!empty($ctx['exception_class'])) {
$entry['error_class'] = (string)$ctx['exception_class'];
}
if (empty($entry['error'])) {
$entry['error'] = 'SERVER_ERROR: ' . $entry['error_internal'];
}
}
if (($ctx['user_agent'] ?? '') !== '') {
@ -563,13 +595,17 @@ function qdb_api_log_resolve_write_path(string $dir): string
*
* @return array{date: string, activity: string, entries: list<array<string, mixed>>, files: list<string>, truncated: bool}
*/
function qdb_api_log_read_entries(string $date, string $activityFilter = '', int $limit = 200): array
function qdb_api_log_read_entries(string $date, string $activityFilter = '', int $limit = 200, bool $errorsOnly = false): array
{
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
$date = date('Y-m-d');
}
$limit = max(1, min(1000, $limit));
$activityFilter = strtolower(trim($activityFilter));
if ($activityFilter === 'errors') {
$errorsOnly = true;
$activityFilter = '';
}
if ($activityFilter !== '' && !in_array($activityFilter, QDB_API_LOG_ACTIVITIES, true)) {
$activityFilter = '';
}
@ -591,6 +627,13 @@ function qdb_api_log_read_entries(string $date, string $activityFilter = '', int
if ($activityFilter !== '' && ($row['activity'] ?? '') !== $activityFilter) {
continue;
}
if ($errorsOnly) {
$rowActivity = $row['activity'] ?? '';
$rowStatus = (int)($row['status'] ?? 0);
if ($rowActivity !== 'api_error' && $rowStatus < 400 && empty($row['error'])) {
continue;
}
}
$entries[] = qdb_api_log_entry_for_ui($row);
}
}
@ -607,13 +650,14 @@ function qdb_api_log_read_entries(string $date, string $activityFilter = '', int
$entries = qdb_api_log_enrich_entries_usernames($entries);
return [
'date' => $date,
'activity' => $activityFilter,
'entries' => $entries,
'files' => array_map('basename', $files),
'truncated' => $truncated,
'kinds' => QDB_API_LOG_ACTIVITIES,
'logStatus' => qdb_api_log_status(),
'date' => $date,
'activity' => $activityFilter,
'errorsOnly' => $errorsOnly,
'entries' => $entries,
'files' => array_map('basename', $files),
'truncated' => $truncated,
'kinds' => QDB_API_LOG_ACTIVITIES,
'logStatus' => qdb_api_log_status(),
];
}
@ -663,6 +707,8 @@ function qdb_api_log_entry_for_ui(array $row): array
'changes' => $changes,
'summary' => qdb_api_log_summarize_changes($changes, $targetUsername),
'error' => $row['error'] ?? null,
'error_code' => $row['error_code'] ?? null,
'error_message' => $row['error_message'] ?? null,
];
}