import type { CliPlugin, PluginConfigValue, PluginContext } from '../types.js' /** * Logger Plugin * Provides structured logging with Pino and configurable output formats */ export const loggerPlugin: CliPlugin = { id: 'logger', category: 'middleware', description: 'Structured logging with Pino and configurable output formats', config: { level: { type: 'select', description: 'Logging level', options: [ { name: 'Fatal', value: 'fatal' }, { name: 'Error', value: 'error' }, { name: 'Warn', value: 'warn' }, { name: 'Info', value: 'info' }, { name: 'Debug', value: 'debug' }, { name: 'Trace', value: 'trace' }, ], default: 'info', }, prettyPrint: { type: 'boolean', description: 'Enable pretty printing for development', default: true, }, logRequests: { type: 'boolean', description: 'Log HTTP requests', default: true, }, logResponses: { type: 'boolean', description: 'Log HTTP responses', default: true, }, logErrors: { type: 'boolean', description: 'Log errors with stack traces', default: true, }, includeHeaders: { type: 'boolean', description: 'Include request/response headers in logs', default: false, }, includeBody: { type: 'boolean', description: 'Include request/response body in logs (development only)', default: false, }, redactFields: { type: 'string', description: 'Comma-separated list of fields to redact (e.g., password,token)', default: 'password,token,authorization,cookie', }, timestampFormat: { type: 'select', description: 'Timestamp format', options: [ { name: 'ISO 8601', value: 'iso' }, { name: 'Unix timestamp', value: 'unix' }, { name: 'Epoch milliseconds', value: 'epoch' }, ], default: 'iso', }, destination: { type: 'select', description: 'Log destination', options: [ { name: 'Console (stdout)', value: 'console' }, { name: 'File', value: 'file' }, { name: 'Both', value: 'both' }, ], default: 'console', }, logFile: { type: 'string', description: 'Log file path (when destination includes file)', default: 'logs/app.log', }, maxFileSize: { type: 'string', description: 'Maximum log file size before rotation (e.g., 10MB)', default: '10MB', }, maxFiles: { type: 'number', description: 'Maximum number of rotated log files to keep', default: 5, }, }, prepare(ctx: PluginContext, config: Record) { const destination = config.destination || 'console' // Add pino dependency ctx.addDependency('pino', '^8.17.2') // Add file rotation dependencies if needed if (destination === 'file' || destination === 'both') { ctx.addDependency('pino-rotating-file-stream', '^0.0.4') } }, async apply(ctx: PluginContext, config: Record): Promise { const level = config.level || 'info' const prettyPrint = config.prettyPrint !== false const logRequests = config.logRequests !== false const logResponses = config.logResponses !== false const logErrors = config.logErrors !== false const includeHeaders = config.includeHeaders || false const includeBody = config.includeBody || false const redactFields = config.redactFields ? String(config.redactFields) .split(',') .map(f => f.trim()) : ['password', 'token', 'authorization', 'cookie'] const timestampFormat = config.timestampFormat || 'iso' const destination = config.destination || 'console' const logFile = config.logFile || 'logs/app.log' const maxFileSize = config.maxFileSize || '10MB' const maxFiles = config.maxFiles || 5 // Generate logger middleware file const loggerMiddlewareContent = `import type { Context, MiddlewareHandler } from 'hono' import { createMiddleware } from 'hono/factory' import { pino } from 'pino' ${destination === 'file' || destination === 'both' ? `import { createWriteStream } from 'pino-rotating-file-stream'` : ''} import type { AppBindings } from './types.js' /** * Logger middleware generated by Logger Plugin */ // Configure Pino logger const loggerConfig: any = { level: '${level}', ${ timestampFormat === 'iso' ? ` timestamp: pino.stdTimeFunctions.isoTime, ` : timestampFormat === 'unix' ? ` timestamp: pino.stdTimeFunctions.unixTime, ` : ` timestamp: pino.stdTimeFunctions.epochTime, ` } redact: { paths: [${redactFields.map(field => `'${field}'`).join(', ')}], censor: '[REDACTED]' }, serializers: { req: (req: any) => ({ method: req.method, url: req.url, ${includeHeaders ? 'headers: req.headers,' : ''} ${includeBody ? 'body: req.body,' : ''} remoteAddress: req.remoteAddress, remotePort: req.remotePort }), res: (res: any) => ({ statusCode: res.statusCode, ${includeHeaders ? 'headers: res.headers,' : ''} ${includeBody ? 'body: res.body,' : ''} }), err: pino.stdSerializers.err } } ${ prettyPrint ? ` // Pretty print for development if (process.env.NODE_ENV !== 'production') { loggerConfig.transport = { target: 'pino-pretty', options: { colorize: true, translateTime: 'HH:MM:ss Z', ignore: 'pid,hostname' } } } ` : '' } ${ destination === 'file' ? ` // File destination const streams = [ createWriteStream({ filename: '${logFile}', size: '${maxFileSize}', maxFiles: ${maxFiles} }) ] export const logger = pino(loggerConfig, pino.multistream(streams)) ` : destination === 'both' ? ` // Both console and file destinations const streams = [ { stream: process.stdout }, createWriteStream({ filename: '${logFile}', size: '${maxFileSize}', maxFiles: ${maxFiles} }) ] export const logger = pino(loggerConfig, pino.multistream(streams)) ` : ` // Console destination export const logger = pino(loggerConfig) ` } /** * Pino logger middleware */ export function pinoLogger(): MiddlewareHandler { return createMiddleware(async (c, next) => { const startTime = Date.now() const method = c.req.method const url = c.req.url const userAgent = c.req.header('user-agent') const ip = c.req.header('cf-connecting-ip') || c.req.header('x-forwarded-for') || 'unknown' const requestId = c.get('requestId') || crypto.randomUUID() // Set request ID for other middleware c.set('requestId', requestId) ${ logRequests ? ` // Log incoming request logger.info({ type: 'request', requestId, method, url: new URL(url).pathname, ip, userAgent, ${includeHeaders ? 'headers: Object.fromEntries(c.req.raw.headers.entries()),' : ''} timestamp: new Date().toISOString() }, \`\${method} \${url}\`) ` : '' } let statusCode: number | undefined let error: unknown = null try { await next() statusCode = c.res.status } catch (err) { error = err statusCode = err instanceof Error && 'status' in err ? (err as any).status : 500 ${ logErrors ? ` // Log error logger.error({ type: 'error', requestId, method, url: new URL(url).pathname, ip, statusCode, error: { message: (err as Error).message, stack: (err as Error).stack, type: err.constructor.name }, duration: Date.now() - startTime, timestamp: new Date().toISOString() }, \`Error in \${method} \${url}: \${(err as Error).message}\`) ` : '' } throw err } finally { const duration = Date.now() - startTime ${ logResponses ? ` // Log response (only if statusCode is available) if (statusCode !== undefined) { const logLevel = statusCode >= 500 ? 'error' : statusCode >= 400 ? 'warn' : 'info' logger[logLevel]({ type: 'response', requestId, method, url: new URL(url).pathname, ip, statusCode, duration, ${includeHeaders ? 'responseHeaders: Object.fromEntries(c.res.headers.entries()),' : ''} timestamp: new Date().toISOString() }, \`\${method} \${url} - \${statusCode} (\${duration}ms)\`) } ` : '' } } }) } // Export logger instance for use in other parts of the application export { logger } ` await ctx.addFile('src/lib/logger.middleware.ts', loggerMiddlewareContent) // Update main app file to include logger middleware if (await ctx.fileExists('src/index.ts')) { await ctx.modifyFile('src/index.ts', (content: string) => { // Add import if not already present if (!content.includes('logger.middleware')) { const importLine = `import { pinoLogger } from './lib/logger.middleware.js'` // Find the last import statement const lines = content.split('\\n') let lastImportIndex = -1 for (let i = 0; i < lines.length; i++) { if (lines[i].trim().startsWith('import ')) { lastImportIndex = i } } if (lastImportIndex >= 0) { lines.splice(lastImportIndex + 1, 0, importLine) } else { lines.unshift(importLine) } content = lines.join('\\n') } // Add middleware usage if not already present if (!content.includes('pinoLogger()')) { // Find where to insert middleware calls (should be first middleware) if (content.includes('const app = ') || content.includes('export const app = ')) { const lines = content.split('\\n') let insertIndex = -1 for (let i = 0; i < lines.length; i++) { if (lines[i].includes('const app = ') || lines[i].includes('export const app = ')) { insertIndex = i + 1 break } } if (insertIndex >= 0) { lines.splice( insertIndex, 0, '', '// Logger middleware (should be first)', "app.use('*', pinoLogger())" ) content = lines.join('\\n') } } } return content }) } }, }