CI Pipeline with Test Management / 🧪 Run Tests & Generate Reports (push) Waiting to run
CI Pipeline with Test Management / 🐳 Docker Integration Tests (push) Blocked by required conditions
CI Pipeline with Test Management / 🏗️ Build Docker Image (push) Blocked by required conditions
CI Pipeline with Test Management / 📊 Generate Test Summary (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🧪 Run Tests & Generate Reports (push) Waiting to run
Test Reporting & Gherkin Analysis / 📊 Analyze Test Coverage (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🔄 Sync with Linear (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / ⚡ Performance Monitoring (push) Blocked by required conditions
- /api/contact enforces per-field length caps (name/email/subject/message) before sanitization/persistence; over-long message returns 400 without touching Mongo (test added) - logger: crypto-based request id (randomUUID) instead of Math.random().substr; defaultMeta.version prefers DEPLOY_VERSION over npm_package_version - next.config: move Turbopack svg rule from deprecated experimental.turbo to top-level turbopack - deps: bump next 15.5.2 → 15.5.23; npm audit fix (19 → 3, remaining are transitive sharp/libvips DoS advisories, not exploitable for static images) Closes MITHOME-78, MITHOME-79
121 lines
3.8 KiB
TypeScript
Executable File
121 lines
3.8 KiB
TypeScript
Executable File
import winston from 'winston'
|
|
import LokiTransport from 'winston-loki'
|
|
import { randomUUID } from 'crypto'
|
|
|
|
const isDevelopment = process.env.NODE_ENV === 'development'
|
|
|
|
// Custom format for structured logging
|
|
const structuredFormat = winston.format.combine(
|
|
winston.format.timestamp({ format: 'ISO' }),
|
|
winston.format.errors({ stack: true }),
|
|
winston.format.json({
|
|
replacer: (_key, value) =>
|
|
typeof value === 'bigint' ? value.toString() : value,
|
|
})
|
|
)
|
|
|
|
// Console format for development
|
|
const consoleFormat = winston.format.combine(
|
|
winston.format.timestamp({ format: 'HH:mm:ss' }),
|
|
winston.format.errors({ stack: true }),
|
|
winston.format.colorize(),
|
|
winston.format.simple(),
|
|
winston.format.printf(({ timestamp, level, message, service, requestId, ...meta }) => {
|
|
const requestInfo = requestId ? `[${requestId}]` : ''
|
|
const serviceInfo = service ? `[${service}]` : '[mozdIT]'
|
|
const metaStr = Object.keys(meta).length ? `\n${JSON.stringify(meta, null, 2)}` : ''
|
|
return `${timestamp} ${serviceInfo} ${level} ${requestInfo} ${message}${metaStr}`
|
|
})
|
|
)
|
|
|
|
// Transports configuration
|
|
const transports: winston.transport[] = [
|
|
// Loki transport for centralized logging
|
|
...(process.env.LOKI_HOST
|
|
? [
|
|
new LokiTransport({
|
|
host: process.env.LOKI_HOST,
|
|
labels: {
|
|
app: 'mozdit-web',
|
|
environment: process.env.NODE_ENV || 'development',
|
|
service: 'frontend'
|
|
},
|
|
basicAuth: process.env.LOKI_USERNAME && process.env.LOKI_PASSWORD
|
|
? `${process.env.LOKI_USERNAME}:${process.env.LOKI_PASSWORD}`
|
|
: undefined,
|
|
json: true,
|
|
format: winston.format.json(),
|
|
onConnectionError: (err: Error) => console.error('Loki connection error:', err)
|
|
})
|
|
]
|
|
: []),
|
|
|
|
// Console for development logging
|
|
new winston.transports.Console({
|
|
level: isDevelopment ? 'debug' : 'info',
|
|
format: isDevelopment ? consoleFormat : structuredFormat,
|
|
handleExceptions: true,
|
|
handleRejections: true
|
|
})
|
|
]
|
|
|
|
// Root logger configuration
|
|
export const logger = winston.createLogger({
|
|
level: isDevelopment ? 'debug' : 'info',
|
|
format: structuredFormat,
|
|
defaultMeta: {
|
|
service: 'mozdit-web',
|
|
version: process.env.DEPLOY_VERSION || process.env.npm_package_version || '1.0.0',
|
|
environment: process.env.NODE_ENV || 'development'
|
|
},
|
|
transports,
|
|
exceptionHandlers: transports,
|
|
rejectionHandlers: transports
|
|
})
|
|
|
|
// Specialized loggers for different components
|
|
export const createComponentLogger = (component: string) => {
|
|
return logger.child({ component })
|
|
}
|
|
|
|
export const requestLogger = logger.child({ component: 'request' })
|
|
export const apiLogger = logger.child({ component: 'api' })
|
|
export const dbLogger = logger.child({ component: 'database' })
|
|
export const authLogger = logger.child({ component: 'auth' })
|
|
export const errorLogger = logger.child({ component: 'error' })
|
|
|
|
// Request ID generator for correlation
|
|
export const generateRequestId = (): string =>
|
|
`${Date.now()}-${randomUUID().slice(0, 8)}`
|
|
|
|
// Helper function for timing operations
|
|
export const withTiming = async <T>(
|
|
operation: string,
|
|
fn: () => Promise<T>,
|
|
metadata: any = {}
|
|
): Promise<T> => {
|
|
const startTime = Date.now()
|
|
logger.debug(`Started ${operation}`, metadata)
|
|
|
|
try {
|
|
const result = await fn()
|
|
const duration = Date.now() - startTime
|
|
logger.debug(`Completed ${operation} in ${duration}ms`, { ...metadata, duration })
|
|
return result
|
|
} catch (error) {
|
|
const duration = Date.now() - startTime
|
|
logger.error(`Failed ${operation} in ${duration}ms`, { ...metadata, duration, error })
|
|
throw error
|
|
}
|
|
}
|
|
|
|
// Graceful shutdown
|
|
const gracefulShutdown = () => {
|
|
logger.info('Initiating graceful shutdown...')
|
|
logger.end()
|
|
}
|
|
|
|
process.on('SIGTERM', gracefulShutdown)
|
|
process.on('SIGINT', gracefulShutdown)
|
|
|
|
export default logger |