refactor(logging): finalize phase 6 error contract and pretty output

This commit is contained in:
Richard R 2026-05-27 06:17:25 -06:00
parent f78d066434
commit 5de9069c20
20 changed files with 239 additions and 55 deletions

View file

@ -52,6 +52,11 @@ const eslintConfig = [
message: message:
"Server logger calls must pass context + message: logger.<level>({ event, ...ctx }, 'message').", "Server logger calls must pass context + message: logger.<level>({ event, ...ctx }, 'message').",
}, },
{
selector: `${SERVER_LOGGER_CALL_SELECTOR}[arguments.0.type!='ObjectExpression']`,
message:
"Server logger first argument must be an object with an event field.",
},
{ {
selector: `${SERVER_LOGGER_CALL_SELECTOR}[arguments.0.type='Literal']`, selector: `${SERVER_LOGGER_CALL_SELECTOR}[arguments.0.type='Literal']`,
message: message:
@ -67,11 +72,36 @@ const eslintConfig = [
message: message:
"Server logger context object must include an event field.", "Server logger context object must include an event field.",
}, },
{
selector: `${SERVER_LOGGER_CALL_SELECTOR}[arguments.1.type!='Literal'][arguments.1.type!='TemplateLiteral']`,
message:
"Server logger second argument must be a message string (literal or template literal).",
},
{
selector: `${STATIC_LOGGER_CALL_SELECTOR}[callee.property.name='error']${LOGGER_RECEIVER_SELECTOR}[arguments.0.type='ObjectExpression']:not(:has(ObjectExpression > Property[key.name='error'])):not(:has(ObjectExpression > SpreadElement))`,
message:
"Error-level server logger calls must include nested `error` payload (prefer `error: errorToLog(...)`).",
},
{
selector: `${SERVER_LOGGER_CALL_SELECTOR} > ObjectExpression:first-child > Property[key.name='detail']`,
message:
"Do not use top-level `detail` in server logger context; keep throwable text under `error.message`.",
},
{ {
selector: `${SERVER_LOGGER_CALL_SELECTOR} > ObjectExpression:first-child > Property[key.name='err']`, selector: `${SERVER_LOGGER_CALL_SELECTOR} > ObjectExpression:first-child > Property[key.name='err']`,
message: message:
"Use `error` (typically from errorToLog(...)) instead of `err` in server logs.", "Use `error` (typically from errorToLog(...)) instead of `err` in server logs.",
}, },
{
selector: `${SERVER_LOGGER_CALL_SELECTOR} > ObjectExpression:first-child > Property[key.name='error'][value.type='Literal']`,
message:
"Server logger `error` must be a structured object (prefer `errorToLog(...)`), not a literal.",
},
{
selector: `${SERVER_LOGGER_CALL_SELECTOR} > ObjectExpression:first-child > Property[key.name='error'][value.type='TemplateLiteral']`,
message:
"Server logger `error` must be a structured object (prefer `errorToLog(...)`), not template text.",
},
], ],
}, },
}, },

View file

@ -333,8 +333,13 @@ export async function POST(request: NextRequest) {
if (!existingResult.settings) { if (!existingResult.settings) {
serverLogger.error({ serverLogger.error({
event: 'audiobook.chapter.meta_settings.invalid', event: 'audiobook.chapter.meta_settings.invalid',
errorCode: 'AUDIOBOOK_CHAPTER_META_SETTINGS_INVALID',
bookId, bookId,
storageUserId, storageUserId,
error: {
name: 'AudiobookMetaSettingsInvalid',
message: 'Invalid audiobook.meta.json settings payload',
},
}, 'Invalid audiobook.meta.json settings payload'); }, 'Invalid audiobook.meta.json settings payload');
return NextResponse.json({ error: 'Invalid audiobook metadata settings' }, { status: 500 }); return NextResponse.json({ error: 'Invalid audiobook metadata settings' }, { status: 500 });
} }

View file

@ -269,10 +269,13 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string
if (closed) return; if (closed) return;
logger.warn({ logger.warn({
event: 'documents.parsed.events.db_resync_failed', event: 'documents.parsed.events.db_resync_failed',
errorCode: 'DOCUMENTS_PARSED_EVENTS_DB_RESYNC_FAILED',
degraded: true,
step: 'db_resync',
documentId: id, documentId: id,
storageUserIdHash, storageUserIdHash,
requestId, requestId,
error: error instanceof Error ? error.message : String(error), error: errorToLog(error),
}, 'SSE DB resync failed'); }, 'SSE DB resync failed');
controller.enqueue(encoder.encode(`event: error\ndata: ${JSON.stringify({ error: String(error) })}\n\n`)); controller.enqueue(encoder.encode(`event: error\ndata: ${JSON.stringify({ error: String(error) })}\n\n`));
}); });
@ -302,6 +305,8 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string
loggedMissingOpId = true; loggedMissingOpId = true;
logger.warn({ logger.warn({
event: 'documents.parsed.events.missing_opid_non_terminal', event: 'documents.parsed.events.missing_opid_non_terminal',
degraded: true,
step: 'poll_without_worker_op',
documentId: id, documentId: id,
storageUserIdHash, storageUserIdHash,
parseStatus: current.parseStatus, parseStatus: current.parseStatus,
@ -339,13 +344,20 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string
if (closed) return; if (closed) return;
if (!response.ok) { if (!response.ok) {
const detail = await response.text().catch(() => ''); const upstreamResponseBody = await response.text().catch(() => '');
logger.warn({ logger.warn({
event: 'documents.parsed.events.worker_stream_request_failed', event: 'documents.parsed.events.worker_stream_request_failed',
errorCode: 'DOCUMENTS_PARSED_EVENTS_WORKER_STREAM_REQUEST_FAILED',
degraded: true,
step: 'worker_stream_request',
documentId: id, documentId: id,
opId: currentOpId, opId: currentOpId,
status: response.status, status: response.status,
detail, upstreamResponseBody,
error: {
name: 'WorkerStreamRequestFailed',
message: `Worker stream request failed with status ${response.status}`,
},
}, 'Worker stream request failed'); }, 'Worker stream request failed');
await sleep(500); await sleep(500);
continue; continue;
@ -353,8 +365,15 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string
if (!response.body) { if (!response.body) {
logger.warn({ logger.warn({
event: 'documents.parsed.events.worker_stream_missing_body', event: 'documents.parsed.events.worker_stream_missing_body',
errorCode: 'DOCUMENTS_PARSED_EVENTS_WORKER_STREAM_MISSING_BODY',
degraded: true,
step: 'worker_stream_body',
documentId: id, documentId: id,
opId: currentOpId, opId: currentOpId,
error: {
name: 'WorkerStreamMissingBody',
message: 'Worker stream response missing body',
},
}, 'Worker stream response missing body'); }, 'Worker stream response missing body');
await sleep(500); await sleep(500);
continue; continue;
@ -455,6 +474,7 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string
logger.error({ logger.error({
event: 'documents.parsed.events.worker_proxy_crashed', event: 'documents.parsed.events.worker_proxy_crashed',
documentId: id, documentId: id,
errorCode: 'DOCUMENTS_PARSED_EVENTS_WORKER_PROXY_CRASHED',
error: errorToLog(error), error: errorToLog(error),
}, 'Worker proxy crashed while streaming parse events'); }, 'Worker proxy crashed while streaming parse events');
if (!closed) { if (!closed) {
@ -485,6 +505,7 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string
} catch (error) { } catch (error) {
logger.error({ logger.error({
event: 'documents.parsed.events.route_failed', event: 'documents.parsed.events.route_failed',
errorCode: 'DOCUMENTS_PARSED_EVENTS_ROUTE_FAILED',
error: errorToLog(error), error: errorToLog(error),
}, 'Parsed events route failed'); }, 'Parsed events route failed');
return NextResponse.json({ error: 'Failed to stream parsed PDF progress' }, { status: 500 }); return NextResponse.json({ error: 'Failed to stream parsed PDF progress' }, { status: 500 });

View file

@ -220,9 +220,16 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string
} }
logger.warn({ logger.warn({
event: 'documents.parsed.requested_op_unavailable', event: 'documents.parsed.requested_op_unavailable',
errorCode: 'DOCUMENTS_PARSED_REQUESTED_OP_UNAVAILABLE',
degraded: true,
step: 'requested_op_lookup',
documentId: id, documentId: id,
userIdHash: hashForLog(row.userId), userIdHash: hashForLog(row.userId),
opId: requestedOpId, opId: requestedOpId,
error: {
name: 'RequestedOperationUnavailable',
message: `Requested worker operation ${requestedOpId} was unavailable`,
},
}, 'Requested worker operation id was unavailable'); }, 'Requested worker operation id was unavailable');
} }
@ -307,6 +314,7 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string
} catch (error) { } catch (error) {
logger.error({ logger.error({
event: 'documents.parsed.get_failed', event: 'documents.parsed.get_failed',
errorCode: 'DOCUMENTS_PARSED_GET_FAILED',
error: errorToLog(error), error: errorToLog(error),
}, 'Failed to read parsed PDF'); }, 'Failed to read parsed PDF');
return NextResponse.json({ error: 'Failed to read parsed PDF' }, { status: 500 }); return NextResponse.json({ error: 'Failed to read parsed PDF' }, { status: 500 });
@ -387,6 +395,7 @@ export async function POST(req: NextRequest, ctx: { params: Promise<{ id: string
} catch (error) { } catch (error) {
logger.error({ logger.error({
event: 'documents.parsed.force_refresh_failed', event: 'documents.parsed.force_refresh_failed',
errorCode: 'DOCUMENTS_PARSED_FORCE_REFRESH_FAILED',
error: errorToLog(error), error: errorToLog(error),
}, 'Failed to force PDF refresh'); }, 'Failed to force PDF refresh');
return NextResponse.json({ error: 'Failed to force PDF refresh' }, { status: 500 }); return NextResponse.json({ error: 'Failed to force PDF refresh' }, { status: 500 });

View file

@ -52,6 +52,7 @@ export async function GET(request: NextRequest) {
} catch (error) { } catch (error) {
logger.error({ logger.error({
event: 'tts.segments.audio_fallback_failed', event: 'tts.segments.audio_fallback_failed',
errorCode: 'TTS_SEGMENT_AUDIO_FALLBACK_FAILED',
error: errorToLog(error), error: errorToLog(error),
}, 'Failed to load segment audio from fallback route'); }, 'Failed to load segment audio from fallback route');
return NextResponse.json({ error: 'Failed to load segment audio' }, { status: 500 }); return NextResponse.json({ error: 'Failed to load segment audio' }, { status: 500 });

View file

@ -41,6 +41,7 @@ export async function POST(request: NextRequest) {
} catch (error) { } catch (error) {
logger.error({ logger.error({
event: 'tts.segments.clear_failed', event: 'tts.segments.clear_failed',
errorCode: 'TTS_SEGMENTS_CLEAR_FAILED',
error: errorToLog(error), error: errorToLog(error),
}, 'Failed to clear TTS segment cache'); }, 'Failed to clear TTS segment cache');
return NextResponse.json({ error: 'Failed to clear TTS segment cache' }, { status: 500 }); return NextResponse.json({ error: 'Failed to clear TTS segment cache' }, { status: 500 });

View file

@ -423,7 +423,10 @@ export async function POST(request: NextRequest) {
documentId: parsed.documentId, documentId: parsed.documentId,
segmentId: segment.segmentId, segmentId: segment.segmentId,
aborted, aborted,
error: alignError instanceof Error ? alignError.message : String(alignError), errorCode: 'TTS_SEGMENT_ALIGNMENT_UNAVAILABLE',
...(aborted ? {} : { degraded: true }),
step: 'whisper_align',
error: errorToLog(alignError),
}, 'Self-heal alignment unavailable'); }, 'Self-heal alignment unavailable');
alignment = null; alignment = null;
} }
@ -665,7 +668,10 @@ export async function POST(request: NextRequest) {
documentId: parsed.documentId, documentId: parsed.documentId,
segmentId: segment.segmentId, segmentId: segment.segmentId,
aborted, aborted,
error: alignError instanceof Error ? alignError.message : String(alignError), errorCode: 'TTS_SEGMENT_ALIGNMENT_UNAVAILABLE',
...(aborted ? {} : { degraded: true }),
step: 'whisper_align',
error: errorToLog(alignError),
}, 'Alignment unavailable'); }, 'Alignment unavailable');
alignment = null; alignment = null;
} }
@ -708,7 +714,7 @@ export async function POST(request: NextRequest) {
status: 'completed', status: 'completed',
}); });
} catch (error) { } catch (error) {
const detail = error instanceof Error ? error.message : 'Failed to generate segment'; const errorMessage = error instanceof Error ? error.message : 'Failed to generate segment';
const aborted = isAbortLikeError(error); const aborted = isAbortLikeError(error);
const upstreamStatus = getUpstreamStatus(error); const upstreamStatus = getUpstreamStatus(error);
const retryAfterSeconds = upstreamStatus === 429 const retryAfterSeconds = upstreamStatus === 429
@ -730,7 +736,7 @@ export async function POST(request: NextRequest) {
stageTimings, stageTimings,
aborted: true, aborted: true,
errorCode, errorCode,
detail, error: errorToLog(error),
completedSoFar: manifest.length, completedSoFar: manifest.length,
totalRequested: normalized.length, totalRequested: normalized.length,
}, 'Stopping segment ensure after abort'); }, 'Stopping segment ensure after abort');
@ -747,7 +753,6 @@ export async function POST(request: NextRequest) {
upstreamStatus, upstreamStatus,
retryAfterSeconds, retryAfterSeconds,
errorCode, errorCode,
detail,
error: errorToLog(error), error: errorToLog(error),
}, 'TTS segment generation failed'); }, 'TTS segment generation failed');
} }
@ -757,8 +762,8 @@ export async function POST(request: NextRequest) {
status: aborted ? 'pending' : 'error', status: aborted ? 'pending' : 'error',
error: aborted ? null : ( error: aborted ? null : (
upstreamStatus upstreamStatus
? `${errorCode}${retryAfterSeconds ? ` (retry after ${retryAfterSeconds}s)` : ''}: ${detail}` ? `${errorCode}${retryAfterSeconds ? ` (retry after ${retryAfterSeconds}s)` : ''}: ${errorMessage}`
: detail : errorMessage
), ),
updatedAt: Date.now(), updatedAt: Date.now(),
}) })
@ -781,7 +786,7 @@ export async function POST(request: NextRequest) {
? null ? null
: { : {
code: errorCode, code: errorCode,
detail, detail: errorMessage,
...(typeof upstreamStatus === 'number' ? { upstreamStatus } : {}), ...(typeof upstreamStatus === 'number' ? { upstreamStatus } : {}),
...(typeof retryAfterSeconds === 'number' ? { retryAfterSeconds } : {}), ...(typeof retryAfterSeconds === 'number' ? { retryAfterSeconds } : {}),
}, },
@ -799,6 +804,7 @@ export async function POST(request: NextRequest) {
if (errorItems.length > 0) { if (errorItems.length > 0) {
logger.error({ logger.error({
event: 'tts.segments.ensure.partial_result', event: 'tts.segments.ensure.partial_result',
errorCode: 'TTS_SEGMENT_PARTIAL_FAILURE',
requestId, requestId,
documentId: parsed.documentId, documentId: parsed.documentId,
total: manifest.length, total: manifest.length,
@ -806,6 +812,10 @@ export async function POST(request: NextRequest) {
pendingCount, pendingCount,
errorCount: errorItems.length, errorCount: errorItems.length,
elapsedMs: Date.now() - requestStartedAt, elapsedMs: Date.now() - requestStartedAt,
error: {
name: 'PartialSegmentFailure',
message: `TTS segment ensure completed with ${errorItems.length} segment errors`,
},
errors: errorItems.slice(0, 5).map((item) => ({ errors: errorItems.slice(0, 5).map((item) => ({
segmentId: item.segmentId, segmentId: item.segmentId,
code: item.error?.code ?? null, code: item.error?.code ?? null,
@ -825,6 +835,7 @@ export async function POST(request: NextRequest) {
} catch (error) { } catch (error) {
logger.error({ logger.error({
event: 'tts.segments.ensure.route_failed', event: 'tts.segments.ensure.route_failed',
errorCode: 'TTS_SEGMENTS_ENSURE_ROUTE_FAILED',
error: errorToLog(error), error: errorToLog(error),
}, 'TTS segments ensure route failed'); }, 'TTS segments ensure route failed');
const response = NextResponse.json({ error: 'Failed to ensure TTS segments' }, { status: 500 }); const response = NextResponse.json({ error: 'Failed to ensure TTS segments' }, { status: 500 });

View file

@ -388,6 +388,7 @@ export async function GET(request: NextRequest) {
} catch (error) { } catch (error) {
logger.error({ logger.error({
event: 'tts.segments.manifest.list_failed', event: 'tts.segments.manifest.list_failed',
errorCode: 'TTS_SEGMENTS_MANIFEST_LIST_FAILED',
error: errorToLog(error), error: errorToLog(error),
}, 'Failed to list TTS segments'); }, 'Failed to list TTS segments');
return NextResponse.json({ error: 'Failed to list TTS segments' }, { status: 500 }); return NextResponse.json({ error: 'Failed to list TTS segments' }, { status: 500 });

View file

@ -63,6 +63,7 @@ export async function syncAdminFlag(
} catch (error) { } catch (error) {
serverLogger.warn({ serverLogger.warn({
event: 'admin.email_sync.user_flag_update.failed', event: 'admin.email_sync.user_flag_update.failed',
errorCode: 'ADMIN_EMAIL_SYNC_USER_FLAG_UPDATE_FAILED',
userIdHash: hashForLog(userId), userIdHash: hashForLog(userId),
degraded: true, degraded: true,
error: errorToLog(error), error: errorToLog(error),

View file

@ -33,6 +33,7 @@ export async function ensureAdminSeed(): Promise<void> {
seedPromise = runSeed().catch((error) => { seedPromise = runSeed().catch((error) => {
serverLogger.warn({ serverLogger.warn({
event: 'admin.seed.run.failed', event: 'admin.seed.run.failed',
errorCode: 'ADMIN_SEED_RUN_FAILED',
degraded: true, degraded: true,
error: errorToLog(error), error: errorToLog(error),
}, 'Admin seed run failed'); }, 'Admin seed run failed');
@ -65,6 +66,7 @@ async function seedDefaultAdminProvider(): Promise<void> {
} catch (error) { } catch (error) {
serverLogger.warn({ serverLogger.warn({
event: 'admin.seed.providers.check_failed', event: 'admin.seed.providers.check_failed',
errorCode: 'ADMIN_SEED_PROVIDERS_CHECK_FAILED',
degraded: true, degraded: true,
error: errorToLog(error), error: errorToLog(error),
}, 'Could not check admin_providers'); }, 'Could not check admin_providers');
@ -102,6 +104,7 @@ async function seedDefaultAdminProvider(): Promise<void> {
} catch (fallbackError) { } catch (fallbackError) {
serverLogger.warn({ serverLogger.warn({
event: 'admin.seed.restrict_user_api_keys.fallback_write_failed', event: 'admin.seed.restrict_user_api_keys.fallback_write_failed',
errorCode: 'ADMIN_SEED_RESTRICT_USER_API_KEYS_FALLBACK_WRITE_FAILED',
degraded: true, degraded: true,
error: errorToLog(fallbackError), error: errorToLog(fallbackError),
}, 'Failed to write restrictUserApiKeys fallback after encryption failure'); }, 'Failed to write restrictUserApiKeys fallback after encryption failure');
@ -109,6 +112,7 @@ async function seedDefaultAdminProvider(): Promise<void> {
} }
serverLogger.warn({ serverLogger.warn({
event: 'admin.seed.provider_key_encrypt.failed', event: 'admin.seed.provider_key_encrypt.failed',
errorCode: 'ADMIN_SEED_PROVIDER_KEY_ENCRYPT_FAILED',
degraded: true, degraded: true,
error: errorToLog(error), error: errorToLog(error),
}, 'Failed to encrypt default provider API key'); }, 'Failed to encrypt default provider API key');
@ -137,6 +141,7 @@ async function seedDefaultAdminProvider(): Promise<void> {
} catch (error) { } catch (error) {
serverLogger.warn({ serverLogger.warn({
event: 'admin.seed.provider_insert.failed', event: 'admin.seed.provider_insert.failed',
errorCode: 'ADMIN_SEED_PROVIDER_INSERT_FAILED',
degraded: true, degraded: true,
providerSlug: 'default-openai', providerSlug: 'default-openai',
error: errorToLog(error), error: errorToLog(error),
@ -166,6 +171,7 @@ async function cleanupLegacyDefaultTtsProviderSeedRow(): Promise<void> {
} catch (error) { } catch (error) {
serverLogger.warn({ serverLogger.warn({
event: 'admin.seed.legacy_default_provider_cleanup.failed', event: 'admin.seed.legacy_default_provider_cleanup.failed',
errorCode: 'ADMIN_SEED_LEGACY_DEFAULT_PROVIDER_CLEANUP_FAILED',
degraded: true, degraded: true,
error: errorToLog(error), error: errorToLog(error),
}, 'Failed to cleanup legacy defaultTtsProvider seed row'); }, 'Failed to cleanup legacy defaultTtsProvider seed row');
@ -180,6 +186,7 @@ async function cleanupLegacyDefaultTtsModelRows(): Promise<void> {
} catch (error) { } catch (error) {
serverLogger.warn({ serverLogger.warn({
event: 'admin.seed.legacy_default_model_cleanup.failed', event: 'admin.seed.legacy_default_model_cleanup.failed',
errorCode: 'ADMIN_SEED_LEGACY_DEFAULT_MODEL_CLEANUP_FAILED',
degraded: true, degraded: true,
error: errorToLog(error), error: errorToLog(error),
}, 'Failed to cleanup legacy defaultTtsModel rows'); }, 'Failed to cleanup legacy defaultTtsModel rows');

View file

@ -116,6 +116,7 @@ async function resolveImplicitDefaultTtsProvider(): Promise<string | undefined>
} catch (error) { } catch (error) {
serverLogger.warn({ serverLogger.warn({
event: 'admin.runtime_config.default_provider_lookup.failed', event: 'admin.runtime_config.default_provider_lookup.failed',
errorCode: 'ADMIN_RUNTIME_CONFIG_DEFAULT_PROVIDER_LOOKUP_FAILED',
degraded: true, degraded: true,
error: errorToLog(error), error: errorToLog(error),
}, 'Implicit defaultTtsProvider lookup failed'); }, 'Implicit defaultTtsProvider lookup failed');
@ -148,6 +149,7 @@ async function readAllRows(): Promise<Map<string, { value: unknown; source: stri
} catch (error) { } catch (error) {
serverLogger.warn({ serverLogger.warn({
event: 'admin.runtime_config.read.failed', event: 'admin.runtime_config.read.failed',
errorCode: 'ADMIN_RUNTIME_CONFIG_READ_FAILED',
degraded: true, degraded: true,
error: errorToLog(error), error: errorToLog(error),
}, 'Runtime config read failed'); }, 'Runtime config read failed');
@ -308,6 +310,7 @@ export async function seedRuntimeConfigFromEnv(): Promise<{ seeded: RuntimeConfi
} catch (error) { } catch (error) {
serverLogger.warn({ serverLogger.warn({
event: 'admin.runtime_config.seed.failed', event: 'admin.runtime_config.seed.failed',
errorCode: 'ADMIN_RUNTIME_CONFIG_SEED_FAILED',
degraded: true, degraded: true,
key, key,
error: errorToLog(error), error: errorToLog(error),

View file

@ -96,6 +96,7 @@ const createAuth = () => betterAuth({
} catch (error) { } catch (error) {
serverLogger.warn({ serverLogger.warn({
event: 'auth.user_delete.storage_cleanup_failed', event: 'auth.user_delete.storage_cleanup_failed',
errorCode: 'AUTH_USER_DELETE_STORAGE_CLEANUP_FAILED',
degraded: true, degraded: true,
step: 'delete_user_storage', step: 'delete_user_storage',
userIdHash: hashForLog(user.id), userIdHash: hashForLog(user.id),
@ -179,6 +180,7 @@ const createAuth = () => betterAuth({
} catch (error) { } catch (error) {
serverLogger.warn({ serverLogger.warn({
event: 'auth.link_account.transfer.rate_limit.failed', event: 'auth.link_account.transfer.rate_limit.failed',
errorCode: 'AUTH_LINK_ACCOUNT_TRANSFER_RATE_LIMIT_FAILED',
degraded: true, degraded: true,
step: 'transfer_rate_limit', step: 'transfer_rate_limit',
anonymousUserIdHash: hashForLog(anonymousUser.user.id), anonymousUserIdHash: hashForLog(anonymousUser.user.id),
@ -202,6 +204,7 @@ const createAuth = () => betterAuth({
} catch (error) { } catch (error) {
serverLogger.warn({ serverLogger.warn({
event: 'auth.link_account.transfer.audiobooks.failed', event: 'auth.link_account.transfer.audiobooks.failed',
errorCode: 'AUTH_LINK_ACCOUNT_TRANSFER_AUDIOBOOKS_FAILED',
degraded: true, degraded: true,
step: 'transfer_audiobooks', step: 'transfer_audiobooks',
anonymousUserIdHash: hashForLog(anonymousUser.user.id), anonymousUserIdHash: hashForLog(anonymousUser.user.id),
@ -225,6 +228,7 @@ const createAuth = () => betterAuth({
} catch (error) { } catch (error) {
serverLogger.warn({ serverLogger.warn({
event: 'auth.link_account.transfer.documents.failed', event: 'auth.link_account.transfer.documents.failed',
errorCode: 'AUTH_LINK_ACCOUNT_TRANSFER_DOCUMENTS_FAILED',
degraded: true, degraded: true,
step: 'transfer_documents', step: 'transfer_documents',
anonymousUserIdHash: hashForLog(anonymousUser.user.id), anonymousUserIdHash: hashForLog(anonymousUser.user.id),
@ -248,6 +252,7 @@ const createAuth = () => betterAuth({
} catch (error) { } catch (error) {
serverLogger.warn({ serverLogger.warn({
event: 'auth.link_account.transfer.preferences.failed', event: 'auth.link_account.transfer.preferences.failed',
errorCode: 'AUTH_LINK_ACCOUNT_TRANSFER_PREFERENCES_FAILED',
degraded: true, degraded: true,
step: 'transfer_preferences', step: 'transfer_preferences',
anonymousUserIdHash: hashForLog(anonymousUser.user.id), anonymousUserIdHash: hashForLog(anonymousUser.user.id),
@ -271,6 +276,7 @@ const createAuth = () => betterAuth({
} catch (error) { } catch (error) {
serverLogger.warn({ serverLogger.warn({
event: 'auth.link_account.transfer.progress.failed', event: 'auth.link_account.transfer.progress.failed',
errorCode: 'AUTH_LINK_ACCOUNT_TRANSFER_PROGRESS_FAILED',
degraded: true, degraded: true,
step: 'transfer_progress', step: 'transfer_progress',
anonymousUserIdHash: hashForLog(anonymousUser.user.id), anonymousUserIdHash: hashForLog(anonymousUser.user.id),
@ -282,6 +288,7 @@ const createAuth = () => betterAuth({
} catch (error) { } catch (error) {
serverLogger.error({ serverLogger.error({
event: 'auth.link_account.failed', event: 'auth.link_account.failed',
errorCode: 'AUTH_LINK_ACCOUNT_FAILED',
error: errorToLog(error), error: errorToLog(error),
}, 'onLinkAccount callback failed'); }, 'onLinkAccount callback failed');
// Don't throw here to prevent blocking the account linking process // Don't throw here to prevent blocking the account linking process

View file

@ -16,8 +16,10 @@ export async function fetchWorkerOperationState<Result>(
} catch (error) { } catch (error) {
serverLogger.warn({ serverLogger.warn({
event: 'compute.worker_op_state.config.invalid', event: 'compute.worker_op_state.config.invalid',
errorCode: 'COMPUTE_WORKER_OP_STATE_CONFIG_INVALID',
opId: normalized, opId: normalized,
degraded: true, degraded: true,
step: 'read_worker_config',
error: errorToLog(error), error: errorToLog(error),
}, 'Worker client env missing/invalid'); }, 'Worker client env missing/invalid');
return null; return null;
@ -38,13 +40,19 @@ export async function fetchWorkerOperationState<Result>(
}); });
if (!res.ok) { if (!res.ok) {
const detail = await res.text().catch(() => ''); const upstreamResponseBody = await res.text().catch(() => '');
serverLogger.warn({ serverLogger.warn({
event: 'compute.worker_op_state.fetch.failed', event: 'compute.worker_op_state.fetch.failed',
errorCode: 'COMPUTE_WORKER_OP_STATE_FETCH_FAILED',
opId: normalized, opId: normalized,
status: res.status, status: res.status,
detail, upstreamResponseBody,
degraded: true, degraded: true,
step: 'fetch_worker_op',
error: {
name: 'WorkerOpStateFetchFailed',
message: `Worker op request failed with status ${res.status}`,
},
}, 'Worker op request failed'); }, 'Worker op request failed');
return null; return null;
} }
@ -52,8 +60,10 @@ export async function fetchWorkerOperationState<Result>(
if (!parsed || typeof parsed !== 'object' || parsed.opId !== normalized) { if (!parsed || typeof parsed !== 'object' || parsed.opId !== normalized) {
serverLogger.warn({ serverLogger.warn({
event: 'compute.worker_op_state.response.invalid', event: 'compute.worker_op_state.response.invalid',
errorCode: 'COMPUTE_WORKER_OP_STATE_RESPONSE_INVALID',
opId: normalized, opId: normalized,
degraded: true, degraded: true,
step: 'validate_worker_op_response',
}, 'Worker op response invalid'); }, 'Worker op response invalid');
return null; return null;
} }
@ -61,8 +71,10 @@ export async function fetchWorkerOperationState<Result>(
} catch (error) { } catch (error) {
serverLogger.warn({ serverLogger.warn({
event: 'compute.worker_op_state.fetch.error', event: 'compute.worker_op_state.fetch.error',
errorCode: 'COMPUTE_WORKER_OP_STATE_FETCH_ERROR',
opId: normalized, opId: normalized,
degraded: true, degraded: true,
step: 'fetch_worker_op',
error: errorToLog(error), error: errorToLog(error),
}, 'Worker op request threw'); }, 'Worker op request threw');
return null; return null;

View file

@ -2,7 +2,7 @@ import { createHash, randomUUID } from 'node:crypto';
import type { ComputeBackend, PdfLayoutInput, WhisperAlignInput, WhisperAlignResult } from '@/lib/server/compute/types'; import type { ComputeBackend, PdfLayoutInput, WhisperAlignInput, WhisperAlignResult } from '@/lib/server/compute/types';
import { parseSseEventId, parseSsePayload } from '@openreader/compute-core'; import { parseSseEventId, parseSsePayload } from '@openreader/compute-core';
import { getWorkerClientWaitTimeoutMs } from '@openreader/compute-core'; import { getWorkerClientWaitTimeoutMs } from '@openreader/compute-core';
import { serverLogger } from '@/lib/server/logger'; import { errorToLog, serverLogger } from '@/lib/server/logger';
import type { import type {
PdfLayoutJobRequest, PdfLayoutJobRequest,
PdfLayoutJobResult, PdfLayoutJobResult,
@ -44,22 +44,6 @@ function truncateForLog(value: string, maxChars = MAX_LOG_DETAIL_CHARS): string
return `${value.slice(0, maxChars)}...`; return `${value.slice(0, maxChars)}...`;
} }
function errorToLog(error: unknown): Record<string, unknown> {
if (error instanceof WorkerHttpError) {
return {
name: error.name,
message: error.message,
status: error.status,
retryAfterMs: error.retryAfterMs,
stack: error.stack,
};
}
if (error instanceof Error) {
return { name: error.name, message: error.message, stack: error.stack };
}
return { message: String(error) };
}
function logWorker(level: WorkerLogLevel, event: string, fields: Record<string, unknown>): void { function logWorker(level: WorkerLogLevel, event: string, fields: Record<string, unknown>): void {
if (!LOG_EVENTS.has(event)) return; if (!LOG_EVENTS.has(event)) return;
if (level === 'error') { if (level === 'error') {
@ -316,6 +300,7 @@ export class WorkerComputeBackend implements ComputeBackend {
return { alignments: final.result.alignments }; return { alignments: final.result.alignments };
}, ({ attempt, maxAttempts, willRetry, delayMs, error }) => { }, ({ attempt, maxAttempts, willRetry, delayMs, error }) => {
logWorker(willRetry ? 'warn' : 'error', 'align.request.attempt_error', { logWorker(willRetry ? 'warn' : 'error', 'align.request.attempt_error', {
errorCode: 'COMPUTE_WORKER_ALIGN_REQUEST_ATTEMPT_ERROR',
traceId, traceId,
kind: 'whisper_align', kind: 'whisper_align',
opKeyHash, opKeyHash,
@ -336,6 +321,7 @@ export class WorkerComputeBackend implements ComputeBackend {
return result; return result;
} catch (error) { } catch (error) {
logWorker('error', 'align.request.failed', { logWorker('error', 'align.request.failed', {
errorCode: 'COMPUTE_WORKER_ALIGN_REQUEST_FAILED',
traceId, traceId,
kind: 'whisper_align', kind: 'whisper_align',
opKeyHash, opKeyHash,
@ -414,6 +400,7 @@ export class WorkerComputeBackend implements ComputeBackend {
throw new Error('PDF layout worker operation completed without parsed output'); throw new Error('PDF layout worker operation completed without parsed output');
}, ({ attempt, maxAttempts, willRetry, delayMs, error }) => { }, ({ attempt, maxAttempts, willRetry, delayMs, error }) => {
logWorker(willRetry ? 'warn' : 'error', 'pdf_layout.request.attempt_error', { logWorker(willRetry ? 'warn' : 'error', 'pdf_layout.request.attempt_error', {
errorCode: 'COMPUTE_WORKER_PDF_LAYOUT_REQUEST_ATTEMPT_ERROR',
traceId, traceId,
kind: 'pdf_layout', kind: 'pdf_layout',
opKeyHash, opKeyHash,
@ -436,6 +423,7 @@ export class WorkerComputeBackend implements ComputeBackend {
return result; return result;
} catch (error) { } catch (error) {
logWorker('error', 'pdf_layout.request.failed', { logWorker('error', 'pdf_layout.request.failed', {
errorCode: 'COMPUTE_WORKER_PDF_LAYOUT_REQUEST_FAILED',
traceId, traceId,
kind: 'pdf_layout', kind: 'pdf_layout',
opKeyHash, opKeyHash,
@ -473,8 +461,14 @@ export class WorkerComputeBackend implements ComputeBackend {
if (!res.ok) { if (!res.ok) {
const retryAfterMs = parseRetryAfterMs(res.headers.get('retry-after')); const retryAfterMs = parseRetryAfterMs(res.headers.get('retry-after'));
const detail = await res.text().catch(() => ''); const upstreamResponseBody = await res.text().catch(() => '');
const requestError = new WorkerHttpError(
`Worker request failed (${method} ${path}): ${res.status}${upstreamResponseBody ? ` ${upstreamResponseBody}` : ''}`,
res.status,
retryAfterMs,
);
logWorker(res.status >= 500 ? 'warn' : 'error', 'http.request.failed', { logWorker(res.status >= 500 ? 'warn' : 'error', 'http.request.failed', {
errorCode: 'COMPUTE_WORKER_HTTP_REQUEST_FAILED',
...context, ...context,
traceId, traceId,
method, method,
@ -482,13 +476,10 @@ export class WorkerComputeBackend implements ComputeBackend {
status: res.status, status: res.status,
retryAfterMs, retryAfterMs,
durationMs: Date.now() - startedAt, durationMs: Date.now() - startedAt,
detail: truncateForLog(detail), upstreamResponseBody: truncateForLog(upstreamResponseBody),
error: errorToLog(requestError),
}); });
throw new WorkerHttpError( throw requestError;
`Worker request failed (${method} ${path}): ${res.status}${detail ? ` ${detail}` : ''}`,
res.status,
retryAfterMs,
);
} }
const parsed = await res.json() as T; const parsed = await res.json() as T;
@ -549,21 +540,24 @@ export class WorkerComputeBackend implements ComputeBackend {
if (!res.ok) { if (!res.ok) {
const retryAfterMs = parseRetryAfterMs(res.headers.get('retry-after')); const retryAfterMs = parseRetryAfterMs(res.headers.get('retry-after'));
const detail = await res.text().catch(() => ''); const upstreamResponseBody = await res.text().catch(() => '');
const requestError = new WorkerHttpError(
`Worker request failed (GET /ops/${encodeURIComponent(opId)}/events): ${res.status}${upstreamResponseBody ? ` ${upstreamResponseBody}` : ''}`,
res.status,
retryAfterMs,
);
logWorker(res.status >= 500 ? 'warn' : 'error', 'sse.wait.http_failed', { logWorker(res.status >= 500 ? 'warn' : 'error', 'sse.wait.http_failed', {
errorCode: 'COMPUTE_WORKER_SSE_WAIT_HTTP_FAILED',
...context, ...context,
traceId, traceId,
opId, opId,
status: res.status, status: res.status,
retryAfterMs, retryAfterMs,
durationMs: Date.now() - startedAt, durationMs: Date.now() - startedAt,
detail: truncateForLog(detail), upstreamResponseBody: truncateForLog(upstreamResponseBody),
error: errorToLog(requestError),
}); });
throw new WorkerHttpError( throw requestError;
`Worker request failed (GET /ops/${encodeURIComponent(opId)}/events): ${res.status}${detail ? ` ${detail}` : ''}`,
res.status,
retryAfterMs,
);
} }
if (!res.body) { if (!res.body) {
@ -675,16 +669,22 @@ export class WorkerComputeBackend implements ComputeBackend {
} }
logWorker('error', 'sse.wait.ended_without_terminal', { logWorker('error', 'sse.wait.ended_without_terminal', {
errorCode: 'COMPUTE_WORKER_SSE_WAIT_ENDED_WITHOUT_TERMINAL',
...context, ...context,
traceId, traceId,
opId, opId,
eventCount, eventCount,
latestStatus: latest?.status ?? null, latestStatus: latest?.status ?? null,
durationMs: Date.now() - startedAt, durationMs: Date.now() - startedAt,
error: {
name: 'WorkerSseWaitEndedWithoutTerminal',
message: `Operation stream ended before terminal state for op ${opId}`,
},
}); });
throw new Error(`Operation stream ended before terminal state for op ${opId}`); throw new Error(`Operation stream ended before terminal state for op ${opId}`);
} catch (error) { } catch (error) {
logWorker('error', 'sse.wait.failed', { logWorker('error', 'sse.wait.failed', {
errorCode: 'COMPUTE_WORKER_SSE_WAIT_FAILED',
...context, ...context,
traceId, traceId,
opId, opId,

View file

@ -377,6 +377,7 @@ export async function ensureDocumentPreview(doc: PreviewSourceDocument, namespac
} catch (error) { } catch (error) {
serverLogger.error({ serverLogger.error({
event: 'documents.preview.generate.failed', event: 'documents.preview.generate.failed',
errorCode: 'DOCUMENTS_PREVIEW_GENERATE_FAILED',
documentId: doc.id, documentId: doc.id,
documentType: doc.type, documentType: doc.type,
error: errorToLog(error), error: errorToLog(error),

View file

@ -371,8 +371,6 @@ export async function parsePdfJob(input: UserPdfLayoutJobRequest): Promise<void>
} }
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : String(error); const message = error instanceof Error ? error.message : String(error);
const stack = error instanceof Error ? error.stack : undefined;
const cause = error instanceof Error ? error.cause : undefined;
const parseStatus = 'failed'; const parseStatus = 'failed';
try { try {
const scopedRows = await loadScopedRows(input); const scopedRows = await loadScopedRows(input);
@ -392,6 +390,7 @@ export async function parsePdfJob(input: UserPdfLayoutJobRequest): Promise<void>
} catch (statusError) { } catch (statusError) {
serverLogger.error({ serverLogger.error({
event: 'documents.parse.status_write.failed', event: 'documents.parse.status_write.failed',
errorCode: 'DOCUMENTS_PARSE_STATUS_WRITE_FAILED',
documentId: input.documentId, documentId: input.documentId,
parseStatus, parseStatus,
error: errorToLog(statusError), error: errorToLog(statusError),
@ -399,11 +398,10 @@ export async function parsePdfJob(input: UserPdfLayoutJobRequest): Promise<void>
} }
serverLogger.error({ serverLogger.error({
event: 'documents.parse.job.failed', event: 'documents.parse.job.failed',
errorCode: 'DOCUMENTS_PARSE_JOB_FAILED',
documentId: input.documentId, documentId: input.documentId,
parseStatus, parseStatus,
error: message, error: errorToLog(error),
...(stack ? { stack } : {}),
...(cause ? { cause: String(cause) } : {}),
}, 'Parse job failed'); }, 'Parse job failed');
} finally { } finally {
running.delete(key); running.delete(key);
@ -417,6 +415,7 @@ export function enqueueParsePdfJob(input: UserPdfLayoutJobRequest): void {
serverLogger.error({ serverLogger.error({
event: 'documents.parse.job.uncaught_error', event: 'documents.parse.job.uncaught_error',
documentId: input.documentId, documentId: input.documentId,
errorCode: 'DOCUMENTS_PARSE_JOB_UNCAUGHT_ERROR',
error: errorToLog(error), error: errorToLog(error),
}, 'Parse job uncaught error'); }, 'Parse job uncaught error');
}); });

View file

@ -33,6 +33,14 @@ export type ServerApiErrorBody = {
details?: Record<string, unknown>; details?: Record<string, unknown>;
}; };
export type LoggedError = {
name: string;
message: string;
code?: string;
stack?: string;
cause?: LoggedError | { message: string };
};
const LOG_FORMAT = process.env.LOG_FORMAT?.trim().toLowerCase() || 'pretty'; const LOG_FORMAT = process.env.LOG_FORMAT?.trim().toLowerCase() || 'pretty';
const LOG_LEVEL = process.env.LOG_LEVEL?.trim() || 'info'; const LOG_LEVEL = process.env.LOG_LEVEL?.trim() || 'info';
@ -59,21 +67,80 @@ function createServerLogger(): ServerLogger {
colorize: true, colorize: true,
translateTime: 'SYS:standard', translateTime: 'SYS:standard',
ignore: 'pid,hostname', ignore: 'pid,hostname',
errorLikeObjectKeys: ['error'],
customPrettifiers: {
error(value) {
if (!value || typeof value !== 'object') return String(value);
const error = value as Record<string, unknown>;
const lines: string[] = [];
if (typeof error.message === 'string' && error.message) lines.push(`error.message: ${error.message}`);
if (typeof error.code === 'string' && error.code) lines.push(`error.code: ${error.code}`);
if (typeof error.name === 'string' && error.name) lines.push(`error.name: ${error.name}`);
if (typeof error.stack === 'string' && error.stack) lines.push(`error.stack:\n${error.stack}`);
if (error.cause !== undefined) {
const causeText = typeof error.cause === 'object'
? JSON.stringify(error.cause)
: String(error.cause);
lines.push(`error.cause: ${causeText}`);
}
if (lines.length > 0) return lines.join('\n');
return JSON.stringify(error);
},
},
}); });
return pino(config, prettyStream); return pino(config, prettyStream);
} }
export const serverLogger: ServerLogger = createServerLogger(); export const serverLogger: ServerLogger = createServerLogger();
export function errorToLog(error: unknown): Record<string, unknown> { function normalizeErrorCode(code: unknown): string | undefined {
if (error instanceof Error) { if (typeof code === 'string' && code.trim()) return code.trim();
if (typeof code === 'number' && Number.isFinite(code)) return String(code);
return undefined;
}
function serializeErrorCause(cause: unknown, depth: number): LoggedError | { message: string } {
if (depth >= 3) {
return { message: String(cause) };
}
if (cause instanceof Error) {
return errorToLog(cause, depth + 1);
}
if (cause && typeof cause === 'object') {
const rec = cause as Record<string, unknown>;
const message = typeof rec.message === 'string' ? rec.message : String(cause);
const name = typeof rec.name === 'string' && rec.name ? rec.name : 'ErrorCause';
const code = normalizeErrorCode(rec.code);
const stack = typeof rec.stack === 'string' ? rec.stack : undefined;
const nestedCause = rec.cause !== undefined ? serializeErrorCause(rec.cause, depth + 1) : undefined;
return { return {
name: error.name, name,
message: error.message, message,
stack: error.stack, ...(code ? { code } : {}),
...(stack ? { stack } : {}),
...(nestedCause ? { cause: nestedCause } : {}),
}; };
} }
return { message: String(error) }; return { message: String(cause) };
}
export function errorToLog(error: unknown, depth = 0): LoggedError {
if (error instanceof Error) {
const rec = error as Error & { code?: unknown; cause?: unknown };
const code = normalizeErrorCode(rec.code);
const cause = rec.cause !== undefined ? serializeErrorCause(rec.cause, depth) : undefined;
return {
name: error.name || 'Error',
message: error.message || 'Unknown error',
...(code ? { code } : {}),
...(error.stack ? { stack: error.stack } : {}),
...(cause ? { cause } : {}),
};
}
return {
name: 'NonErrorThrowable',
message: String(error),
};
} }
export function apiErrorBody( export function apiErrorBody(

View file

@ -68,6 +68,7 @@ export async function clearTtsSegmentCache(
warning = error instanceof Error ? error.message : 'Failed deleting some audio objects'; warning = error instanceof Error ? error.message : 'Failed deleting some audio objects';
serverLogger.warn({ serverLogger.warn({
event: 'tts.segments.cache.audio_cleanup_failed', event: 'tts.segments.cache.audio_cleanup_failed',
errorCode: 'TTS_SEGMENTS_CACHE_AUDIO_CLEANUP_FAILED',
degraded: true, degraded: true,
step: 'delete_tts_audio_objects', step: 'delete_tts_audio_objects',
documentId: input.documentId, documentId: input.documentId,

View file

@ -195,6 +195,7 @@ async function fetchReplicateOpenApiSchema(apiKey: string, model: string): Promi
} }
serverLogger.error({ serverLogger.error({
event: 'tts.voice_resolution.replicate_schema_fetch.failed', event: 'tts.voice_resolution.replicate_schema_fetch.failed',
errorCode: 'TTS_VOICE_RESOLUTION_REPLICATE_SCHEMA_FETCH_FAILED',
error: errorToLog(error), error: errorToLog(error),
}, 'Failed fetching Replicate model schema'); }, 'Failed fetching Replicate model schema');
return null; return null;
@ -289,6 +290,7 @@ async function fetchDeepinfraVoices(apiKey: string): Promise<string[]> {
} }
serverLogger.error({ serverLogger.error({
event: 'tts.voice_resolution.deepinfra_voices_fetch.failed', event: 'tts.voice_resolution.deepinfra_voices_fetch.failed',
errorCode: 'TTS_VOICE_RESOLUTION_DEEPINFRA_VOICES_FETCH_FAILED',
error: errorToLog(error), error: errorToLog(error),
}, 'Failed fetching Deepinfra voices'); }, 'Failed fetching Deepinfra voices');
return []; return [];

View file

@ -52,6 +52,7 @@ export async function deleteUserStorageData(
} catch (error) { } catch (error) {
serverLogger.warn({ serverLogger.warn({
event: 'user.data_cleanup.document_blob_delete.failed', event: 'user.data_cleanup.document_blob_delete.failed',
errorCode: 'USER_DATA_CLEANUP_DOCUMENT_BLOB_DELETE_FAILED',
degraded: true, degraded: true,
step: 'delete_document_blob', step: 'delete_document_blob',
documentId: doc.id, documentId: doc.id,
@ -65,6 +66,7 @@ export async function deleteUserStorageData(
} catch (error) { } catch (error) {
serverLogger.warn({ serverLogger.warn({
event: 'user.data_cleanup.document_preview_delete.failed', event: 'user.data_cleanup.document_preview_delete.failed',
errorCode: 'USER_DATA_CLEANUP_DOCUMENT_PREVIEW_DELETE_FAILED',
degraded: true, degraded: true,
step: 'delete_document_preview_artifacts', step: 'delete_document_preview_artifacts',
documentId: doc.id, documentId: doc.id,
@ -80,6 +82,7 @@ export async function deleteUserStorageData(
} catch (error) { } catch (error) {
serverLogger.warn({ serverLogger.warn({
event: 'user.data_cleanup.document_preview_rows_delete.failed', event: 'user.data_cleanup.document_preview_rows_delete.failed',
errorCode: 'USER_DATA_CLEANUP_DOCUMENT_PREVIEW_ROWS_DELETE_FAILED',
degraded: true, degraded: true,
step: 'delete_document_preview_rows', step: 'delete_document_preview_rows',
documentId: doc.id, documentId: doc.id,
@ -106,6 +109,7 @@ export async function deleteUserStorageData(
} catch (error) { } catch (error) {
serverLogger.warn({ serverLogger.warn({
event: 'user.data_cleanup.audiobook_blobs_delete.failed', event: 'user.data_cleanup.audiobook_blobs_delete.failed',
errorCode: 'USER_DATA_CLEANUP_AUDIOBOOK_BLOBS_DELETE_FAILED',
degraded: true, degraded: true,
step: 'delete_audiobook_prefix', step: 'delete_audiobook_prefix',
bookId: book.id, bookId: book.id,
@ -128,6 +132,7 @@ export async function deleteUserStorageData(
} catch (error) { } catch (error) {
serverLogger.warn({ serverLogger.warn({
event: 'user.data_cleanup.tts_segments_delete.failed', event: 'user.data_cleanup.tts_segments_delete.failed',
errorCode: 'USER_DATA_CLEANUP_TTS_SEGMENTS_DELETE_FAILED',
degraded: true, degraded: true,
step: 'delete_tts_segment_prefixes', step: 'delete_tts_segment_prefixes',
userIdHash: hashForLog(userId), userIdHash: hashForLog(userId),