refactor: Clean up logging system with LOG_LEVEL filtering

- Add LOG_LEVEL env variable support (debug, info, warn, error)
- Default to 'info' level for production-ready logs
- Integrate Morgan HTTP logging with custom logger
- Remove console.logs and replace with custom logger
- Remove sensitive password debug logs from email service
- Remove noisy warn logs from email sync and event notifier
- Add gray color for timestamps to improve readability

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
richardtekula
2025-12-17 09:45:00 +01:00
parent f8d8bb2330
commit bd44458c32
10 changed files with 66 additions and 44 deletions

View File

@@ -10,6 +10,7 @@ import { validateBody } from './middlewares/global/validateBody.js';
import { notFound } from './middlewares/global/notFound.js'; import { notFound } from './middlewares/global/notFound.js';
import { errorHandler } from './middlewares/global/errorHandler.js'; import { errorHandler } from './middlewares/global/errorHandler.js';
import { apiRateLimiter } from './middlewares/security/rateLimiter.js'; import { apiRateLimiter } from './middlewares/security/rateLimiter.js';
import { logger } from './utils/logger.js';
// Import routes // Import routes
import authRoutes from './routes/auth.routes.js'; import authRoutes from './routes/auth.routes.js';
@@ -29,8 +30,9 @@ import eventRoutes from './routes/event.routes.js';
const app = express(); const app = express();
// Security middleware // HTTP request logging via Morgan -> custom logger
app.use(morgan('dev')); const morganStream = { write: (message) => logger.http(message.trim()) };
app.use(morgan(':method :url :status :response-time ms', { stream: morganStream }));
app.use( app.use(
helmet({ helmet({
contentSecurityPolicy: { contentSecurityPolicy: {

View File

@@ -15,9 +15,9 @@ const pool = new Pool({
connectionTimeoutMillis: 2000, connectionTimeoutMillis: 2000,
}); });
// Note: Connection logging handled in index.js to avoid circular dependencies // Critical database errors - use console.error directly to avoid circular imports
pool.on('error', (err) => { pool.on('error', (err) => {
console.error('Neočakávaná chyba databázy:', err); console.error('[FATAL] Database connection error:', err.message);
process.exit(-1); process.exit(-1);
}); });

View File

@@ -332,7 +332,6 @@ export const sendSingleEventNotification = async (eventId, adminUserId) => {
const userEmail = await getUserEmail(userId); const userEmail = await getUserEmail(userId);
if (!userEmail) { if (!userEmail) {
logger.warn(`Používateľ ${username} nemá nastavený primárny email - preskakujem`);
stats.skipped++; stats.skipped++;
continue; continue;
} }
@@ -343,7 +342,7 @@ export const sendSingleEventNotification = async (eventId, adminUserId) => {
const textBody = generateEventNotificationText({ firstName, username, event }); const textBody = generateEventNotificationText({ firstName, username, event });
// Send email // Send email
logger.info(`Odosielam notifikáciu pre ${username} (${userEmail})`); logger.info(`Odosielam notifikáciu pre ${username}`);
const success = await sendNotificationEmail(jmapConfig, userEmail, subject, htmlBody, textBody); const success = await sendNotificationEmail(jmapConfig, userEmail, subject, htmlBody, textBody);
@@ -427,7 +426,6 @@ export const sendEventNotifications = async () => {
const userEmail = await getUserEmail(userId); const userEmail = await getUserEmail(userId);
if (!userEmail) { if (!userEmail) {
logger.warn(`Používateľ ${username} nemá nastavený primárny email - preskakujem`);
stats.skipped++; stats.skipped++;
continue; continue;
} }
@@ -438,7 +436,7 @@ export const sendEventNotifications = async () => {
const textBody = generateEventNotificationText({ firstName, username, event }); const textBody = generateEventNotificationText({ firstName, username, event });
// Send email // Send email
logger.info(`Odosielam notifikáciu pre ${username} (${userEmail}) - udalosť: ${event.title}`); logger.info(`Odosielam notifikáciu pre ${username} - udalosť: ${event.title}`);
const success = await sendNotificationEmail(jmapConfig, userEmail, subject, htmlBody, textBody); const success = await sendNotificationEmail(jmapConfig, userEmail, subject, htmlBody, textBody);

View File

@@ -1,9 +1,10 @@
import app from './app.js'; import app from './app.js';
import { startAllCronJobs } from './cron/index.js'; import { startAllCronJobs } from './cron/index.js';
import { logger } from './utils/logger.js';
const port = process.env.PORT || 5000; const port = process.env.PORT || 5000;
app.listen(port, () => { app.listen(port, () => {
console.log(`🚀 Server running on http://localhost:${port}`); logger.info(`Server running on http://localhost:${port}`);
// Start cron jobs after server is running // Start cron jobs after server is running
startAllCronJobs(); startAllCronJobs();

View File

@@ -1,6 +1,7 @@
import { db } from '../../config/database.js'; import { db } from '../../config/database.js';
import { companyUsers, projectUsers, todoUsers } from '../../db/schema.js'; import { companyUsers, projectUsers, todoUsers } from '../../db/schema.js';
import { eq, and } from 'drizzle-orm'; import { eq, and } from 'drizzle-orm';
import { logger } from '../../utils/logger.js';
/** /**
* Univerzálny middleware pre kontrolu prístupu k resources * Univerzálny middleware pre kontrolu prístupu k resources
@@ -104,7 +105,7 @@ export const checkResourceAccess = (resourceType, paramName) => {
next(); next();
} catch (error) { } catch (error) {
console.error('Resource access check error:', error); logger.error('Resource access check error', error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
error: { error: {

View File

@@ -409,19 +409,8 @@ export const removeUserFromEmailAccount = async (accountId, userId) => {
* Get email account with decrypted password (for JMAP operations) * Get email account with decrypted password (for JMAP operations)
*/ */
export const getEmailAccountWithCredentials = async (accountId, userId) => { export const getEmailAccountWithCredentials = async (accountId, userId) => {
logger.debug('getEmailAccountWithCredentials called', { accountId, userId });
const account = await getEmailAccountById(accountId, userId); const account = await getEmailAccountById(accountId, userId);
logger.debug('Account retrieved', {
id: account.id,
email: account.email,
hasPassword: !!account.emailPassword,
passwordLength: account.emailPassword?.length
});
const decryptedPassword = decryptPassword(account.emailPassword); const decryptedPassword = decryptPassword(account.emailPassword);
logger.debug('Password decrypted', { passwordLength: decryptedPassword?.length });
return { return {
id: account.id, id: account.id,

View File

@@ -147,7 +147,6 @@ export const syncEmailsFromSender = async (
// Skip emails without from or to (malformed data) // Skip emails without from or to (malformed data)
if (!fromEmail && !toEmail) { if (!fromEmail && !toEmail) {
logger.warn(`Preskakujem email ${messageId} - chýba pole from aj to`);
continue; continue;
} }
@@ -160,7 +159,6 @@ export const syncEmailsFromSender = async (
toEmail?.toLowerCase() === senderEmail.toLowerCase(); toEmail?.toLowerCase() === senderEmail.toLowerCase();
if (!belongsToContact) { if (!belongsToContact) {
logger.warn(`Preskakujem email ${messageId} - nepatrí kontaktu ${senderEmail} (od: ${fromEmail}, pre: ${toEmail})`);
continue; continue;
} }

View File

@@ -4,6 +4,7 @@ import { db } from '../config/database.js';
import { timesheets, users } from '../db/schema.js'; import { timesheets, users } from '../db/schema.js';
import { and, desc, eq } from 'drizzle-orm'; import { and, desc, eq } from 'drizzle-orm';
import { BadRequestError, ForbiddenError, NotFoundError } from '../utils/errors.js'; import { BadRequestError, ForbiddenError, NotFoundError } from '../utils/errors.js';
import { logger } from '../utils/logger.js';
const ALLOWED_MIME_TYPES = [ const ALLOWED_MIME_TYPES = [
'application/pdf', 'application/pdf',
@@ -71,8 +72,7 @@ const safeUnlink = async (filePath) => {
try { try {
await fs.unlink(filePath); await fs.unlink(filePath);
} catch (error) { } catch (error) {
// Keep server responsive even if cleanup fails logger.error('Failed to delete file', error);
console.error('Nepodarilo sa zmazať súbor:', error);
} }
}; };

View File

@@ -142,7 +142,6 @@ export const getTodoById = async (todoId) => {
*/ */
export const createTodo = async (userId, data) => { export const createTodo = async (userId, data) => {
const { title, description, projectId, companyId, assignedUserIds, status, priority, dueDate } = data; const { title, description, projectId, companyId, assignedUserIds, status, priority, dueDate } = data;
console.log('Service createTodo - assignedUserIds:', assignedUserIds);
// Verify project exists if provided // Verify project exists if provided
if (projectId) { if (projectId) {
@@ -205,10 +204,7 @@ export const createTodo = async (userId, data) => {
assignedBy: userId, assignedBy: userId,
})); }));
console.log('Inserting todo_users:', todoUserInserts);
await db.insert(todoUsers).values(todoUserInserts); await db.insert(todoUsers).values(todoUserInserts);
} else {
console.log('No users to assign - assignedUserIds:', assignedUserIds);
} }
return newTodo; return newTodo;

View File

@@ -1,7 +1,22 @@
/** /**
* Simple logger utility * Logger utility with LOG_LEVEL filtering
*
* LOG_LEVEL options (from lowest to highest):
* - debug: Show all logs
* - info: Show info, success, warn, error, audit
* - warn: Show warn, error, audit
* - error: Show only errors and audit
*
* Default: 'info' (production-ready default)
*/ */
const LOG_LEVELS = {
debug: 0,
info: 1,
warn: 2,
error: 3,
};
const colors = { const colors = {
reset: '\x1b[0m', reset: '\x1b[0m',
red: '\x1b[31m', red: '\x1b[31m',
@@ -10,37 +25,51 @@ const colors = {
blue: '\x1b[34m', blue: '\x1b[34m',
magenta: '\x1b[35m', magenta: '\x1b[35m',
cyan: '\x1b[36m', cyan: '\x1b[36m',
gray: '\x1b[90m',
}; };
const getTimestamp = () => { const getTimestamp = () => {
return new Date().toISOString(); return new Date().toISOString();
}; };
const getCurrentLevel = () => {
const level = process.env.LOG_LEVEL?.toLowerCase() || 'info';
return LOG_LEVELS[level] ?? LOG_LEVELS.info;
};
const shouldLog = (level) => {
return LOG_LEVELS[level] <= getCurrentLevel();
};
export const logger = { export const logger = {
info: (message, ...args) => { info: (message, ...args) => {
if (!shouldLog('info')) return;
console.log( console.log(
`${colors.blue}[INFO]${colors.reset} ${getTimestamp()} - ${message}`, `${colors.blue}[INFO]${colors.reset} ${colors.gray}${getTimestamp()}${colors.reset} ${message}`,
...args ...args
); );
}, },
success: (message, ...args) => { success: (message, ...args) => {
if (!shouldLog('info')) return;
console.log( console.log(
`${colors.green}[SUCCESS]${colors.reset} ${getTimestamp()} - ${message}`, `${colors.green}[SUCCESS]${colors.reset} ${colors.gray}${getTimestamp()}${colors.reset} ${message}`,
...args ...args
); );
}, },
warn: (message, ...args) => { warn: (message, ...args) => {
if (!shouldLog('warn')) return;
console.warn( console.warn(
`${colors.yellow}[WARN]${colors.reset} ${getTimestamp()} - ${message}`, `${colors.yellow}[WARN]${colors.reset} ${colors.gray}${getTimestamp()}${colors.reset} ${message}`,
...args ...args
); );
}, },
error: (message, error, ...args) => { error: (message, error, ...args) => {
if (!shouldLog('error')) return;
console.error( console.error(
`${colors.red}[ERROR]${colors.reset} ${getTimestamp()} - ${message}`, `${colors.red}[ERROR]${colors.reset} ${colors.gray}${getTimestamp()}${colors.reset} ${message}`,
...args ...args
); );
if (error && process.env.NODE_ENV === 'development') { if (error && process.env.NODE_ENV === 'development') {
@@ -49,18 +78,26 @@ export const logger = {
}, },
debug: (message, ...args) => { debug: (message, ...args) => {
if (process.env.NODE_ENV === 'development') { if (!shouldLog('debug')) return;
console.log( console.log(
`${colors.cyan}[DEBUG]${colors.reset} ${getTimestamp()} - ${message}`, `${colors.cyan}[DEBUG]${colors.reset} ${colors.gray}${getTimestamp()}${colors.reset} ${message}`,
...args ...args
); );
}
}, },
audit: (message, ...args) => { audit: (message, ...args) => {
// Audit logs always show regardless of level
console.log( console.log(
`${colors.magenta}[AUDIT]${colors.reset} ${getTimestamp()} - ${message}`, `${colors.magenta}[AUDIT]${colors.reset} ${colors.gray}${getTimestamp()}${colors.reset} ${message}`,
...args ...args
); );
}, },
// HTTP request logger (for Morgan integration)
http: (message) => {
if (!shouldLog('info')) return;
console.log(
`${colors.gray}[HTTP]${colors.reset} ${colors.gray}${getTimestamp()}${colors.reset} ${message}`
);
},
}; };