Implement centralized logging with RMABLogger

Replaces scattered console statements with a unified RMABLogger across backend API routes and services. Adds LOG_LEVEL-based filtering, job-aware database persistence, and context-based logging. Updates documentation to describe the new logging system and usage patterns. Also documents qBittorrent CSRF header fix
This commit is contained in:
kikootwo
2026-01-12 12:45:48 -05:00
parent ba5f5cf7d6
commit 682836237b
118 changed files with 1623 additions and 1079 deletions
+6 -3
View File
@@ -7,6 +7,9 @@ import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db';
import { getEncryptionService } from '@/lib/services/encryption.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.BookDateConfig');
// GET: Fetch global BookDate configuration (excluding API key)
// Any authenticated user can check if BookDate is configured
@@ -24,7 +27,7 @@ async function getConfig(req: AuthenticatedRequest) {
return NextResponse.json({ config: safeConfig });
} catch (error: any) {
console.error('[BookDate] Get config error:', error);
logger.error('Get config error', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json(
{ error: error.message || 'Failed to fetch configuration' },
{ status: 500 }
@@ -129,7 +132,7 @@ async function saveConfig(req: AuthenticatedRequest) {
});
} catch (error: any) {
console.error('[BookDate] Save config error:', error);
logger.error('Save config error', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json(
{ error: error.message || 'Failed to save configuration' },
{ status: 500 }
@@ -162,7 +165,7 @@ async function deleteConfig(req: AuthenticatedRequest) {
return NextResponse.json({ success: true });
} catch (error: any) {
console.error('[BookDate] Delete config error:', error);
logger.error('Delete config error', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json(
{ error: error.message || 'Failed to delete configuration' },
{ status: 500 }
+10 -7
View File
@@ -14,6 +14,9 @@ import {
isAlreadyRequested,
isAlreadySwiped,
} from '@/lib/bookdate/helpers';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.BookDate.Generate');
async function handler(req: AuthenticatedRequest) {
try {
@@ -54,7 +57,7 @@ async function handler(req: AuthenticatedRequest) {
};
// Build prompt and call AI (same as recommendations endpoint, but doesn't check cache)
console.log('[BookDate] Force generating new recommendations for user:', userId);
logger.info('Force generating new recommendations for user', { userId });
const prompt = await buildAIPrompt(userId, userPreferences);
const aiResponse = await callAI(config.provider, config.model, config.apiKey, prompt);
@@ -62,7 +65,7 @@ async function handler(req: AuthenticatedRequest) {
throw new Error('Invalid AI response format: missing recommendations array');
}
console.log(`[BookDate] AI returned ${aiResponse.recommendations.length} recommendations`);
logger.debug('AI returned recommendations', { count: aiResponse.recommendations.length });
// Match to Audnexus and filter
const batchId = `batch_${Date.now()}`;
@@ -88,14 +91,14 @@ async function handler(req: AuthenticatedRequest) {
const audnexusMatch = await matchToAudnexus(rec.title, rec.author);
if (!audnexusMatch) {
console.warn(`[BookDate] No Audnexus match: "${rec.title}" by ${rec.author}`);
logger.warn('No Audnexus match', { title: rec.title, author: rec.author });
continue;
}
// Check again if in library with ASIN for exact matching
// This catches books that might have different titles (e.g., "The Tenant" vs "The Tenant (Unabridged)")
if (await isInLibrary(userId, audnexusMatch.title, audnexusMatch.author, audnexusMatch.asin)) {
console.log(`[BookDate] Book "${audnexusMatch.title}" (ASIN: ${audnexusMatch.asin}) is in library, skipping`);
logger.debug('Book is in library, skipping', { title: audnexusMatch.title, asin: audnexusMatch.asin });
continue;
}
@@ -122,12 +125,12 @@ async function handler(req: AuthenticatedRequest) {
}
} catch (error) {
console.warn(`[BookDate] Match error for "${rec.title}":`, error);
logger.warn('Match error', { title: rec.title, error: error instanceof Error ? error.message : String(error) });
continue;
}
}
console.log(`[BookDate] Matched ${matched.length} new recommendations`);
logger.info('Matched new recommendations', { count: matched.length });
if (matched.length === 0) {
return NextResponse.json(
@@ -163,7 +166,7 @@ async function handler(req: AuthenticatedRequest) {
});
} catch (error: any) {
console.error('[BookDate] Generate error:', error);
logger.error('Generate error', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json(
{
error: error.message || 'Failed to generate new recommendations',
+5 -2
View File
@@ -7,6 +7,9 @@ import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db';
import { getConfigService } from '@/lib/services/config.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.BookDate.Preferences');
/**
* GET /api/bookdate/preferences
@@ -54,7 +57,7 @@ async function getPreferences(req: AuthenticatedRequest) {
});
} catch (error: any) {
console.error('Get BookDate preferences error:', error);
logger.error('Get BookDate preferences error', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json(
{ error: error.message || 'Failed to get preferences' },
{ status: 500 }
@@ -135,7 +138,7 @@ async function updatePreferences(req: AuthenticatedRequest) {
});
} catch (error: any) {
console.error('Update BookDate preferences error:', error);
logger.error('Update BookDate preferences error', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json(
{ error: error.message || 'Failed to update preferences' },
{ status: 500 }
+14 -11
View File
@@ -14,6 +14,9 @@ import {
isAlreadyRequested,
isAlreadySwiped,
} from '@/lib/bookdate/helpers';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.BookDate.Recommendations');
async function handler(req: AuthenticatedRequest) {
try {
@@ -75,7 +78,7 @@ async function handler(req: AuthenticatedRequest) {
};
// Build prompt and call AI
console.log('[BookDate] Generating new recommendations for user:', userId);
logger.info('Generating new recommendations for user', { userId });
const prompt = await buildAIPrompt(userId, userPreferences);
const aiResponse = await callAI(config.provider, config.model, config.apiKey, prompt);
@@ -83,7 +86,7 @@ async function handler(req: AuthenticatedRequest) {
throw new Error('Invalid AI response format: missing recommendations array');
}
console.log(`[BookDate] AI returned ${aiResponse.recommendations.length} recommendations`);
logger.debug('AI returned recommendations', { count: aiResponse.recommendations.length });
// Match to Audnexus and filter
const batchId = `batch_${Date.now()}`;
@@ -91,19 +94,19 @@ async function handler(req: AuthenticatedRequest) {
for (const rec of aiResponse.recommendations) {
if (!rec.title || !rec.author) {
console.warn('[BookDate] Skipping recommendation with missing title or author');
logger.warn('Skipping recommendation with missing title or author');
continue;
}
// Check if already swiped
if (await isAlreadySwiped(userId, rec.title, rec.author)) {
console.log(`[BookDate] Skipping already swiped: "${rec.title}"`);
logger.debug('Skipping already swiped', { title: rec.title });
continue;
}
// Check if in library
if (await isInLibrary(userId, rec.title, rec.author)) {
console.log(`[BookDate] Skipping already in library: "${rec.title}"`);
logger.debug('Skipping already in library', { title: rec.title });
continue;
}
@@ -112,20 +115,20 @@ async function handler(req: AuthenticatedRequest) {
const audnexusMatch = await matchToAudnexus(rec.title, rec.author);
if (!audnexusMatch) {
console.warn(`[BookDate] No Audnexus match: "${rec.title}" by ${rec.author}`);
logger.warn('No Audnexus match', { title: rec.title, author: rec.author });
continue;
}
// Check again if in library with ASIN for exact matching
// This catches books that might have different titles (e.g., "The Tenant" vs "The Tenant (Unabridged)")
if (await isInLibrary(userId, audnexusMatch.title, audnexusMatch.author, audnexusMatch.asin)) {
console.log(`[BookDate] Book "${audnexusMatch.title}" (ASIN: ${audnexusMatch.asin}) is in library, skipping`);
logger.debug('Book is in library, skipping', { title: audnexusMatch.title, asin: audnexusMatch.asin });
continue;
}
// Check if already requested
if (await isAlreadyRequested(userId, audnexusMatch.asin)) {
console.log(`[BookDate] Skipping already requested: "${rec.title}"`);
logger.debug('Skipping already requested', { title: rec.title });
continue;
}
@@ -147,12 +150,12 @@ async function handler(req: AuthenticatedRequest) {
}
} catch (error) {
console.warn(`[BookDate] Match error for "${rec.title}":`, error);
logger.warn('Match error', { title: rec.title, error: error instanceof Error ? error.message : String(error) });
continue;
}
}
console.log(`[BookDate] Matched ${matched.length} recommendations`);
logger.info('Matched recommendations', { count: matched.length });
// Save to database
if (matched.length > 0) {
@@ -180,7 +183,7 @@ async function handler(req: AuthenticatedRequest) {
});
} catch (error: any) {
console.error('[BookDate] Recommendations error:', error);
logger.error('Recommendations error', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json(
{
error: error.message || 'Failed to generate recommendations',
+7 -4
View File
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.BookDateSwipe');
async function handler(req: AuthenticatedRequest) {
try {
@@ -97,7 +100,7 @@ async function handler(req: AuthenticatedRequest) {
},
});
console.log(`[BookDate] Created request for "${recommendation.title}"`);
logger.info(`Created request for "${recommendation.title}"`);
// Trigger search job (same as regular request creation)
const { getJobQueueService } = await import('@/lib/services/job-queue.service');
@@ -108,11 +111,11 @@ async function handler(req: AuthenticatedRequest) {
author: audiobook.author,
});
console.log(`[BookDate] Triggered search job for request ${newRequest.id}`);
logger.info(`Triggered search job for request ${newRequest.id}`);
}
} catch (error) {
console.error('[BookDate] Error creating request:', error);
logger.error('Error creating request', { error: error instanceof Error ? error.message : String(error) });
// Don't fail the swipe if request creation fails
}
}
@@ -124,7 +127,7 @@ async function handler(req: AuthenticatedRequest) {
});
} catch (error: any) {
console.error('[BookDate] Swipe error:', error);
logger.error('Swipe error', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json(
{ error: error.message || 'Failed to record swipe' },
{ status: 500 }
+5 -2
View File
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.BookDate.Swipes');
// DELETE: Clear all users' swipe history (Admin only)
async function clearSwipes(req: AuthenticatedRequest) {
@@ -16,7 +19,7 @@ async function clearSwipes(req: AuthenticatedRequest) {
// Also clear all cached recommendations (since swipe history affects recommendations)
await prisma.bookDateRecommendation.deleteMany({});
console.log('[BookDate] Admin cleared all swipe history and recommendations');
logger.info('Admin cleared all swipe history and recommendations');
return NextResponse.json({
success: true,
@@ -24,7 +27,7 @@ async function clearSwipes(req: AuthenticatedRequest) {
});
} catch (error: any) {
console.error('[BookDate] Clear swipes error:', error);
logger.error('Clear swipes error', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json(
{ error: error.message || 'Failed to clear swipe history' },
{ status: 500 }
@@ -5,6 +5,9 @@
import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.BookDate.TestConnection');
async function authenticatedHandler(req: AuthenticatedRequest) {
try {
@@ -64,7 +67,7 @@ async function authenticatedHandler(req: AuthenticatedRequest) {
if (!response.ok) {
const errorText = await response.text();
console.error('[BookDate] OpenAI API error:', errorText);
logger.error('OpenAI API error', { error: errorText });
return NextResponse.json(
{ error: 'Invalid OpenAI API key or connection failed' },
{ status: 400 }
@@ -108,7 +111,7 @@ async function authenticatedHandler(req: AuthenticatedRequest) {
if (!response.ok) {
const errorText = await response.text();
console.error('[BookDate] Claude API error:', errorText);
logger.error('Claude API error', { error: errorText });
return NextResponse.json(
{ error: 'Invalid Claude API key or connection failed' },
{ status: 400 }
@@ -123,7 +126,7 @@ async function authenticatedHandler(req: AuthenticatedRequest) {
});
} catch (error: any) {
console.error('[BookDate] Test connection error:', error);
logger.error('Test connection error', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json(
{ error: error.message || 'Connection test failed' },
{ status: 500 }
@@ -179,7 +182,7 @@ async function unauthenticatedHandler(req: NextRequest) {
if (!response.ok) {
const errorText = await response.text();
console.error('[BookDate] OpenAI API error:', errorText);
logger.error('OpenAI API error', { error: errorText });
return NextResponse.json(
{ error: 'Invalid OpenAI API key or connection failed' },
{ status: 400 }
@@ -223,7 +226,7 @@ async function unauthenticatedHandler(req: NextRequest) {
if (!response.ok) {
const errorText = await response.text();
console.error('[BookDate] Claude API error:', errorText);
logger.error('Claude API error', { error: errorText });
return NextResponse.json(
{ error: 'Invalid Claude API key or connection failed' },
{ status: 400 }
@@ -238,7 +241,7 @@ async function unauthenticatedHandler(req: NextRequest) {
});
} catch (error: any) {
console.error('[BookDate] Test connection error:', error);
logger.error('Test connection error', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json(
{ error: error.message || 'Connection test failed' },
{ status: 500 }
+4 -1
View File
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.BookDate.Undo');
async function handler(req: AuthenticatedRequest) {
try {
@@ -77,7 +80,7 @@ async function handler(req: AuthenticatedRequest) {
});
} catch (error: any) {
console.error('[BookDate] Undo error:', error);
logger.error('Undo error', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json(
{ error: error.message || 'Failed to undo swipe' },
{ status: 500 }