如何实现Bunyan日志通用对象序列化器且不修改调用方式?
Got it, let's figure out how to build a Bunyan serializer that handles any object type without changing how you call your logging functions. I've tackled this exact problem before, so here's a practical, step-by-step solution:
Bunyan uses built-in serializers for common types like err, req, and res, but falls back to plain JSON.stringify for other objects. This breaks with:
- Circular references
- Non-enumerable properties
- Special types like
Date,Map,Set, or custom classes - Objects with nested complex structures
Our goal is to intercept all object processing before Bunyan does its default serialization—without touching existing log calls.
First, write a recursive function that handles all edge cases while preserving object structure:
const bunyan = require('bunyan'); function processUniversalObject(value, seen = new WeakSet(), depthLimit = 10, currentDepth = 0) { // Stop recursion if we hit depth limit to avoid performance hits if (currentDepth >= depthLimit) return '[Depth Limit Reached]'; // Non-object values get returned as-is if (value === null || typeof value !== 'object') return value; // Handle circular references if (seen.has(value)) return '[Circular Reference]'; seen.add(value); // Reuse Bunyan's excellent built-in Error serializer if (value instanceof Error) { seen.delete(value); return bunyan.stdSerializers.err(value); } // Handle special native types switch (true) { case value instanceof Date: seen.delete(value); return value.toISOString(); case value instanceof RegExp: seen.delete(value); return value.toString(); case value instanceof Map: seen.delete(value); return Object.fromEntries(value); case value instanceof Set: seen.delete(value); return Array.from(value); } // Process plain objects (including non-enumerable properties and Symbols) const processedObj = {}; // Handle regular properties Object.getOwnPropertyNames(value).forEach(key => { processedObj[key] = processUniversalObject(value[key], seen, depthLimit, currentDepth + 1); }); // Handle Symbol properties Object.getOwnPropertySymbols(value).forEach(symbol => { processedObj[symbol.toString()] = processUniversalObject(value[symbol], seen, depthLimit, currentDepth + 1); }); seen.delete(value); return processedObj; }
We'll extend Bunyan's base Logger class to intercept log calls, process all fields automatically, and pass them to the original Bunyan logic. This way, you don't have to change any existing logger.info()/logger.error() calls:
class UniversalBunyanLogger extends bunyan.Logger { log(level, ...args) { let fields = {}; let msg = ''; const restArgs = []; // Parse arguments to match Bunyan's native API if (typeof args[0] === 'object' && args[0] !== null) { fields = args[0]; if (args.length > 1 && typeof args[1] === 'string') { msg = args[1]; restArgs.push(...args.slice(2)); } else { restArgs.push(...args.slice(1)); } } else { msg = args[0]; restArgs.push(...args.slice(1)); } // Process all fields with our universal serializer const processedFields = processUniversalObject(fields); // Call Bunyan's original log method with processed data return super.log(level, processedFields, msg, ...restArgs); } }
Initialize your custom logger just like a regular Bunyan logger—all existing log calls will work exactly as before, but with proper serialization:
// Create your logger instance const logger = new UniversalBunyanLogger({ name: 'my-app', level: 'info', // Keep Bunyan's default serializers for req/res if needed serializers: bunyan.stdSerializers }); // Test with all kinds of objects—no changes to your existing code! const circularObj = {}; circularObj.self = circularObj; logger.info({ user: { id: 1, name: 'Alice', createdAt: new Date() }, settings: new Map([['theme', 'dark'], ['notifications', true]]), error: new Error('Failed to load data'), circular: circularObj, // Non-enumerable property example hiddenProp: Object.defineProperty({}, 'secret', { value: '123', enumerable: false }).secret }, 'User session initialized'); // Simple string message with attached object (works too!) logger.info('Processing complete', { payload: { status: 'success', data: new Set([1, 2, 3]) } });
- Adjust Depth Limit: Tweak the
depthLimitparameter inprocessUniversalObjectto balance detail and performance for your use case. - Add Custom Class Handling: Add a case for your own classes (e.g.,
case value instanceof User) to serialize only relevant properties instead of the entire object. - Performance Tuning: If you're logging high volumes of data, add checks to skip processing for simple objects (e.g., plain objects with only primitive values).
内容的提问来源于stack exchange,提问作者Diego

