Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
165 lines
4.6 KiB
JavaScript
165 lines
4.6 KiB
JavaScript
// backend/utils/logger.js
|
|
// LOG-001: Pino Logger - ADR-015
|
|
// Created: 2026-01-08
|
|
|
|
import pino from 'pino';
|
|
import crypto from 'crypto';
|
|
|
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
const isTest = process.env.NODE_ENV === 'test' || process.env.TEST_MODE === 'true';
|
|
|
|
/**
|
|
* Main application logger
|
|
* Structured JSON logging with Pino
|
|
*/
|
|
export const logger = pino({
|
|
level: process.env.LOG_LEVEL || (isProduction ? 'info' : isTest ? 'silent' : 'debug'),
|
|
|
|
// Pretty print in development only
|
|
transport: (!isProduction && !isTest) ? {
|
|
target: 'pino-pretty',
|
|
options: {
|
|
colorize: true,
|
|
translateTime: 'SYS:standard',
|
|
ignore: 'pid,hostname'
|
|
}
|
|
} : undefined,
|
|
|
|
// Base fields for all logs
|
|
base: {
|
|
service: 'godcrm',
|
|
version: process.env.npm_package_version || '0.0.0',
|
|
env: process.env.NODE_ENV || 'development'
|
|
},
|
|
|
|
// Redact sensitive information
|
|
redact: {
|
|
paths: [
|
|
'req.headers.authorization',
|
|
'req.headers.cookie',
|
|
'req.body.password',
|
|
'req.body.currentPassword',
|
|
'req.body.newPassword',
|
|
'password',
|
|
'password_hash',
|
|
'refreshToken',
|
|
'accessToken',
|
|
'token',
|
|
'apiKey',
|
|
'api_key',
|
|
'secret',
|
|
'encryption_key',
|
|
'encryption_key_encrypted'
|
|
],
|
|
remove: true
|
|
},
|
|
|
|
// Serializers for common objects
|
|
serializers: {
|
|
req: (req) => ({
|
|
method: req.method,
|
|
url: req.url,
|
|
path: req.path,
|
|
query: req.query,
|
|
ip: req.ip || req.headers['x-forwarded-for'],
|
|
userId: req.user?.id
|
|
}),
|
|
res: (res) => ({
|
|
statusCode: res.statusCode
|
|
}),
|
|
err: pino.stdSerializers.err
|
|
}
|
|
});
|
|
|
|
// ============================================================
|
|
// Module-specific child loggers
|
|
// ============================================================
|
|
|
|
export const dbLogger = logger.child({ module: 'database' });
|
|
export const authLogger = logger.child({ module: 'auth' });
|
|
export const apiLogger = logger.child({ module: 'api' });
|
|
export const webhookLogger = logger.child({ module: 'webhooks' });
|
|
export const fileLogger = logger.child({ module: 'files' });
|
|
export const aiLogger = logger.child({ module: 'ai' });
|
|
export const syncLogger = logger.child({ module: 'sync' });
|
|
|
|
// ============================================================
|
|
// Request logging middleware
|
|
// ============================================================
|
|
|
|
/**
|
|
* Express middleware for request logging
|
|
* Adds requestId and logs request/response
|
|
* @param {import('express').Request} req
|
|
* @param {import('express').Response} res
|
|
* @param {import('express').NextFunction} next
|
|
*/
|
|
export function requestLogger(req, res, next) {
|
|
const startTime = Date.now();
|
|
|
|
// Generate request ID if not present
|
|
req.id = req.headers['x-request-id'] || crypto.randomUUID();
|
|
res.setHeader('x-request-id', req.id);
|
|
|
|
// Create request-scoped logger
|
|
req.log = apiLogger.child({ requestId: req.id });
|
|
|
|
// Log on response finish
|
|
res.on('finish', () => {
|
|
const duration = Date.now() - startTime;
|
|
const logData = {
|
|
requestId: req.id,
|
|
method: req.method,
|
|
url: req.originalUrl,
|
|
statusCode: res.statusCode,
|
|
duration,
|
|
userId: req.user?.id,
|
|
ip: req.ip || req.headers['x-forwarded-for']?.split(',')[0]
|
|
};
|
|
|
|
if (res.statusCode >= 500) {
|
|
apiLogger.error(logData, 'Request error');
|
|
} else if (res.statusCode >= 400) {
|
|
apiLogger.warn(logData, 'Request warning');
|
|
} else {
|
|
apiLogger.info(logData, 'Request completed');
|
|
}
|
|
});
|
|
|
|
next();
|
|
}
|
|
|
|
// ============================================================
|
|
// Utility functions
|
|
// ============================================================
|
|
|
|
/**
|
|
* Create a child logger for a specific operation
|
|
* @param {string} operation - Operation name
|
|
* @param {Object} context - Additional context
|
|
* @returns {pino.Logger}
|
|
*/
|
|
export function createOperationLogger(operation, context = {}) {
|
|
return logger.child({ operation, ...context });
|
|
}
|
|
|
|
/**
|
|
* Log database query (with timing)
|
|
* @param {string} query - SQL query (sanitized)
|
|
* @param {number} duration - Query duration in ms
|
|
* @param {Object} context - Additional context
|
|
*/
|
|
export function logQuery(query, duration, context = {}) {
|
|
dbLogger.debug({ query: query.substring(0, 200), duration, ...context }, 'SQL Query');
|
|
}
|
|
|
|
/**
|
|
* Log authentication event
|
|
* @param {string} event - Event type (login, logout, failed_login, etc.)
|
|
* @param {Object} context - Event context
|
|
*/
|
|
export function logAuthEvent(event, context = {}) {
|
|
authLogger.info({ event, ...context }, `Auth: ${event}`);
|
|
}
|
|
|
|
export default logger;
|