diff --git a/src/app/employer/documents/components/AgentChatInterface.tsx b/src/app/employer/documents/components/AgentChatInterface.tsx index 3474bc6..83f84da 100644 --- a/src/app/employer/documents/components/AgentChatInterface.tsx +++ b/src/app/employer/documents/components/AgentChatInterface.tsx @@ -3,6 +3,7 @@ import { Brain, Send, ThumbsUp, ThumbsDown, Plus, Search, ExternalLink } from 'l import { useAgentChatbot, type Message } from '../hooks/useAgentChatbot'; import MarkdownMessage from "~/app/_components/MarkdownMessage"; import clsx from 'clsx'; +import { logger } from '~/lib/logger'; interface AgentChatInterfaceProps { chatId: string; @@ -59,7 +60,9 @@ export const AgentChatInterface: React.FC = ({ if (welcomeMsg) { setMessages([welcomeMsg]); } - }).catch(console.error); + }).catch((err) => { + logger.error('Failed to send welcome message', { chatId, aiPersona }, err); + }); } }; void loadAndCheckWelcome(); @@ -150,9 +153,12 @@ export const AgentChatInterface: React.FC = ({ aiPersona: aiPersona, // Include AI persona for learning coach mode }; - // Console log web search status - console.log('🔍 Frontend: enableWebSearch =', enableWebSearch, 'type:', typeof enableWebSearch); - console.log('📤 Frontend: Sending requestBody with enableWebSearch:', requestBody.enableWebSearch); + // Log web search status for debugging + logger.debug('Web search status', { + enableWebSearch, + enableWebSearchType: typeof enableWebSearch, + requestBodyWebSearch: requestBody.enableWebSearch + }); if (searchScope === "document" && selectedDocId) { requestBody.documentId = selectedDocId; @@ -198,7 +204,7 @@ export const AgentChatInterface: React.FC = ({ } } } catch (err) { - console.error('AI service error:', err); + logger.error('AI service error', { chatId, documentId: selectedDocId, companyId }, err); // Send error message const errorResponse = await sendMessage({ chatId, @@ -211,7 +217,7 @@ export const AgentChatInterface: React.FC = ({ } } } catch (err) { - console.error('Failed to send message:', err); + logger.error('Failed to send message', { chatId, input: input.substring(0, 50) }, err); } finally { setIsSubmitting(false); } diff --git a/src/app/employer/documents/fetchWithRetries.ts b/src/app/employer/documents/fetchWithRetries.ts index b2ee214..d30df84 100644 --- a/src/app/employer/documents/fetchWithRetries.ts +++ b/src/app/employer/documents/fetchWithRetries.ts @@ -1,3 +1,5 @@ +import { logger } from '~/lib/logger'; + // 1) A helper function that retries fetch up to `maxRetries` times export async function fetchWithRetries( url: string, @@ -35,7 +37,7 @@ export async function fetchWithRetries( /timed out/i.test(err.message) || err.name === "AbortError"; if (isTimeoutError && attempt < maxRetries) { - console.warn(`Attempt ${attempt} failed due to timeout, retrying...`); + logger.warn(`Fetch attempt failed due to timeout, retrying`, { url, attempt, maxRetries }, err); continue; // Go to the next attempt } diff --git a/src/lib/api-utils.ts b/src/lib/api-utils.ts index 095bb7a..8bce3b3 100644 --- a/src/lib/api-utils.ts +++ b/src/lib/api-utils.ts @@ -1,5 +1,6 @@ import { NextResponse } from "next/server"; import { HTTP_STATUS, ERROR_TYPES, type ErrorType } from "./constants"; +import { logger } from "./logger"; /** * Utility functions for API responses @@ -139,7 +140,7 @@ export function createExternalServiceError( * Handle API errors with appropriate response */ export function handleApiError(error: unknown): NextResponse { - console.error("API Error:", error); + logger.error("API Error", {}, error instanceof Error ? error : String(error)); if (error instanceof Error) { if (error.message.includes('timed out') || error.message.includes('timeout')) { diff --git a/src/lib/logger.ts b/src/lib/logger.ts new file mode 100644 index 0000000..3a4ccc7 --- /dev/null +++ b/src/lib/logger.ts @@ -0,0 +1,170 @@ +/** + * Structured Logging Service + * + * A centralized logging utility that provides consistent logging across the application + * with support for different log levels, request tracing, and environment-aware behavior. + */ + +export type LogLevel = 'debug' | 'info' | 'warn' | 'error'; + +export interface LogContext { + requestId?: string; + userId?: string; + documentId?: number; + companyId?: number; + [key: string]: unknown; +} + +interface LogEntry { + level: LogLevel; + message: string; + context?: LogContext; + timestamp: string; + error?: Error | string; +} + +class Logger { + private isDevelopment: boolean; + private isProduction: boolean; + + constructor() { + this.isDevelopment = process.env.NODE_ENV === 'development'; + this.isProduction = process.env.NODE_ENV === 'production'; + } + + /** + * Format log entry for output + */ + private formatLogEntry(entry: LogEntry): string { + const { level, message, context, timestamp, error } = entry; + + const parts = [ + `[${timestamp}]`, + `[${level.toUpperCase()}]`, + message, + ]; + + if (context && Object.keys(context).length > 0) { + parts.push(`| Context: ${JSON.stringify(context)}`); + } + + if (error) { + parts.push(`| Error: ${error instanceof Error ? error.message : String(error)}`); + if (this.isDevelopment && error instanceof Error && error.stack) { + parts.push(`\n${error.stack}`); + } + } + + return parts.join(' '); + } + + /** + * Core logging method + */ + private log(level: LogLevel, message: string, context?: LogContext, error?: Error | string): void { + // In production, only log warnings and errors + if (this.isProduction && (level === 'debug' || level === 'info')) { + return; + } + + const entry: LogEntry = { + level, + message, + context, + timestamp: new Date().toISOString(), + error, + }; + + const formattedMessage = this.formatLogEntry(entry); + + switch (level) { + case 'debug': + console.debug(formattedMessage); + break; + case 'info': + console.info(formattedMessage); + break; + case 'warn': + console.warn(formattedMessage); + break; + case 'error': + console.error(formattedMessage); + break; + } + } + + /** + * Log debug information (development only) + */ + debug(message: string, context?: LogContext): void { + this.log('debug', message, context); + } + + /** + * Log general information + */ + info(message: string, context?: LogContext): void { + this.log('info', message, context); + } + + /** + * Log warnings + */ + warn(message: string, context?: LogContext, error?: Error | string): void { + this.log('warn', message, context, error); + } + + /** + * Log errors + */ + error(message: string, context?: LogContext, error?: Error | string): void { + this.log('error', message, context, error); + } + + /** + * Create a child logger with persistent context + */ + withContext(baseContext: LogContext): ChildLogger { + return new ChildLogger(this, baseContext); + } +} + +/** + * Child logger that maintains a base context + */ +class ChildLogger { + constructor( + private parent: Logger, + private baseContext: LogContext + ) {} + + private mergeContext(additionalContext?: LogContext): LogContext { + return { ...this.baseContext, ...additionalContext }; + } + + debug(message: string, context?: LogContext): void { + this.parent.debug(message, this.mergeContext(context)); + } + + info(message: string, context?: LogContext): void { + this.parent.info(message, this.mergeContext(context)); + } + + warn(message: string, context?: LogContext, error?: Error | string): void { + this.parent.warn(message, this.mergeContext(context), error); + } + + error(message: string, context?: LogContext, error?: Error | string): void { + this.parent.error(message, this.mergeContext(context), error); + } + + withContext(additionalContext: LogContext): ChildLogger { + return new ChildLogger(this.parent, this.mergeContext(additionalContext)); + } +} + +// Export singleton instance +export const logger = new Logger(); + +// Export for testing purposes +export { Logger, ChildLogger };