Console Interception
The B3API automatically intercepts all console.log, console.error, console.warn, and console.debug calls throughout the codebase, redirecting them to the Pino JSON logger. This provides structured logging without requiring code changes to existing console statements.
Overview
Zero Migration Required: All existing console.* calls automatically output structured JSON logs to Loki/Grafana.
// Your existing code - works as-is!
console.log('User logged in', { user_id: 123 });
console.error('Database error', error);
console.warn('Deprecated API call');
console.debug('Cache miss', { key: 'user:123' });All console calls are automatically transformed into structured JSON logs with:
- Appropriate log levels (info, error, warn, debug)
- Timestamps and service metadata
- Preserved data and context
- Stack traces for errors
How It Works
Initialization
Console interception is enabled once at application startup:
// src/index.ts (Main API)
import { interceptConsole } from './utils/console-interceptor';
// Set up console interception before any other code runs
interceptConsole();
// Now all console.* calls output to Pino JSON loggerImplementation
The interceptor saves the original console methods and replaces them with Pino equivalents:
// src/utils/console-interceptor.ts
import { logger } from './logger';
const originalConsole = {
log: console.log,
error: console.error,
warn: console.warn,
debug: console.debug
};
export function interceptConsole() {
console.log = (...args: any[]) => {
const { msg, data } = formatArgs(args);
logger.info({ msg, console_data: data, source: 'console.log' });
};
console.error = (...args: any[]) => {
const { msg, data, error } = formatArgs(args);
if (error) {
logger.error({
msg,
error: error.message,
stack: error.stack,
console_data: data,
source: 'console.error'
});
} else {
logger.error({ msg, console_data: data, source: 'console.error' });
}
};
console.warn = (...args: any[]) => {
const { msg, data } = formatArgs(args);
logger.warn({ msg, console_data: data, source: 'console.warn' });
};
console.debug = (...args: any[]) => {
const { msg, data } = formatArgs(args);
logger.debug({ msg, console_data: data, source: 'console.debug' });
};
}Log Transformations
console.log() → Pino info
Original Code:
console.log('Server is running at http://localhost:3000');JSON Output:
{
"level": "info",
"time": "2025-12-16T10:30:45.123Z",
"pid": 1234,
"hostname": "api-server-01",
"service": "b3api",
"msg": "Server is running at http://localhost:3000",
"source": "console.log"
}console.log() with Objects
Original Code:
console.log('User logged in', { user_id: 123, email: 'user@example.com' });JSON Output:
{
"level": "info",
"time": "2025-12-16T10:30:45.456Z",
"service": "b3api",
"msg": "User logged in",
"console_data": {
"user_id": 123,
"email": "user@example.com"
},
"source": "console.log"
}console.error() → Pino error
Original Code:
const error = new Error('Connection timeout');
console.error('Database connection failed', error);JSON Output:
{
"level": "error",
"time": "2025-12-16T10:30:45.789Z",
"service": "b3api",
"msg": "Database connection failed",
"error": "Connection timeout",
"stack": "Error: Connection timeout\n at Database.connect (/app/src/database.ts:45:13)\n at async Application.start (/app/src/index.ts:30:5)",
"source": "console.error"
}console.warn() → Pino warn
Original Code:
console.warn('API rate limit approaching', { current: 950, limit: 1000 });JSON Output:
{
"level": "warn",
"time": "2025-12-16T10:30:46.000Z",
"service": "b3api",
"msg": "API rate limit approaching",
"console_data": {
"current": 950,
"limit": 1000
},
"source": "console.warn"
}console.debug() → Pino debug
Original Code:
console.debug('Cache miss', { key: 'user:123', ttl: 3600 });JSON Output:
{
"level": "debug",
"time": "2025-12-16T10:30:46.200Z",
"service": "b3api",
"msg": "Cache miss",
"console_data": {
"key": "user:123",
"ttl": 3600
},
"source": "console.debug"
}Where Interception is Enabled
Console interception is automatically enabled in all application entry points:
- ✅ Main API Server (
src/index.ts) - ✅ Cron Worker (
src/services/workers/cron/cron-worker.ts) - ✅ Cron Scheduler (
src/services/workers/cron/cron-scheduler.ts) - ✅ Email Service (
src/services/workers/emailService/emailService.ts)
All console calls in these processes (and their dependencies) are automatically intercepted and logged as JSON.
Examples by Data Type
Simple Strings
console.log('Application started');Output:
{
"level": "info",
"msg": "Application started",
"source": "console.log"
}String with Data Object
console.log('Processing order', { order_id: 456, amount: 99.99 });Output:
{
"level": "info",
"msg": "Processing order",
"console_data": {
"order_id": 456,
"amount": 99.99
},
"source": "console.log"
}Multiple Arguments
console.log('User', { id: 123, name: 'John' }, 'performed action', { action: 'login' });Output:
{
"level": "info",
"msg": "User",
"console_data": [
{ "id": 123, "name": "John" },
"performed action",
{ "action": "login" }
],
"source": "console.log"
}Error Objects
try {
// ... some operation
} catch (err) {
console.error('Operation failed', err);
}Output:
{
"level": "error",
"msg": "Operation failed",
"error": "Connection refused",
"stack": "Error: Connection refused\n at ...",
"source": "console.error"
}Arrays
console.log('Active users', [123, 456, 789]);Output:
{
"level": "info",
"msg": "Active users",
"console_data": [123, 456, 789],
"source": "console.log"
}Complex Objects
console.log({
event: 'payment_processed',
user_id: 123,
amount: 99.99,
currency: 'USD',
items: [{ id: 1, name: 'Product A' }]
});Output:
{
"level": "info",
"msg": "Console output",
"console_data": {
"event": "payment_processed",
"user_id": 123,
"amount": 99.99,
"currency": "USD",
"items": [{ "id": 1, "name": "Product A" }]
},
"source": "console.log"
}Querying Console Logs
All console.log Calls
{service="b3api"} | json | source="console.log"All console.error Calls
{service="b3api"} | json | source="console.error"All Console Calls (Any Type)
{service="b3api"} | json | source=~"console.*"Search by Message Content
{service="b3api"} | json | source="console.log" | msg=~".*User.*"Find Logs with Specific Data
{service="b3api"} | json | console_data_user_id="123"Compare Console vs Logger Calls
# Only console calls
{service="b3api"} | json | source=~"console.*"
# Only direct logger calls
{service="b3api"} | json | source!~"console.*"Migration Path (Optional)
You don't need to migrate! Console interception means existing code works automatically. However, for new code, using the logger directly provides benefits:
Before (Auto-Intercepted)
console.log('User created', { user_id: 123 });Output:
{
"level": "info",
"msg": "User created",
"console_data": { "user_id": 123 },
"source": "console.log"
}After (Direct Logger - Preferred for New Code)
import { logger } from './utils/logger';
logger.info({ msg: 'User created', user_id: 123 });Output:
{
"level": "info",
"msg": "User created",
"user_id": 123
}Benefits of Direct Logger:
- ✅ No "console_data" wrapper
- ✅ Better TypeScript typing
- ✅ More control over log structure
- ✅ Explicit log levels
- ✅ Support for child loggers
Both approaches work! The choice depends on your preference and whether you're updating existing code or writing new code.
Disabling Interception (Advanced)
If needed, you can restore original console behavior:
import { restoreConsole } from './utils/console-interceptor';
// Restore original console methods
restoreConsole();
// Now console.log outputs to stdout instead of Pino
console.log('This goes to stdout');Accessing Original Console (Advanced)
For debugging purposes, you can access the original console:
import { getOriginalConsole } from './utils/console-interceptor';
const original = getOriginalConsole();
// This bypasses Pino and goes directly to stdout
original.log('Debug message to stdout');
original.error('Error to stderr');Use cases:
- Debugging the logging system itself
- Development/testing scenarios
- Special cases requiring stdout output
Best Practices
✅ Good - Keep Using Console as Normal
console.log('Order processed', { order_id: 123 });
console.error('Payment failed', error);
console.warn('Low inventory', { product_id: 456 });These all work perfectly and output structured JSON!
✅ Better - Use Logger Directly for New Code
import { logger } from './utils/logger';
logger.info({ msg: 'Order processed', order_id: 123 });
logger.error({ msg: 'Payment failed', error: error.message, stack: error.stack });
logger.warn({ msg: 'Low inventory', product_id: 456 });Provides cleaner output structure and better TypeScript support.
❌ Avoid - Don't Mix Console and Logger for Same Message
// Don't do this - creates duplicate logs
console.log('Starting task');
logger.info({ msg: 'Starting task' });Choose one approach per log statement.
Benefits
Zero Code Changes
All existing console.* calls work unchanged and output JSON automatically.
Backward Compatible
No breaking changes - legacy code continues to work.
Structured Data
Objects, arrays, and complex data structures are preserved.
Error Context
Error objects are automatically formatted with stack traces.
Source Tracking
The source field identifies which console method was called.
Production Ready
JSON format is perfect for Loki/Grafana integration.
Implementation Details
Argument Formatting
The interceptor intelligently formats console arguments:
- Single string: Used as the message
- String + objects: String is message, objects go to console_data
- Multiple args: First string/arg is message, rest go to console_data array
- Error objects: Extracted separately with message and stack trace
- Plain objects: Wrapped in console_data
Source Attribution
Every intercepted log includes a source field:
console.log→source: "console.log"console.error→source: "console.error"console.warn→source: "console.warn"console.debug→source: "console.debug"
This allows you to:
- Track which console method was used
- Filter logs by console type
- Identify areas that could benefit from direct logger usage
Error Handling
Error objects are specially handled:
console.error('Database error', new Error('Connection refused'));Produces:
{
"level": "error",
"msg": "Database error",
"error": "Connection refused",
"stack": "Error: Connection refused\n at ...",
"source": "console.error"
}Stack traces are always preserved!
Debugging Tips
Identify Intercepted Logs
All console-intercepted logs have the source field:
{service="b3api"} | json | source=~"console.*"Find Areas Using Console
To find code still using console methods:
# Group by source to see distribution
sum by (source) (count_over_time({service="b3api"} | json | source=~"console.*" [1h]))Track Migration Progress
Compare console vs direct logger usage:
# Console calls
count_over_time({service="b3api"} | json | source=~"console.*" [1d])
# Direct logger calls
count_over_time({service="b3api"} | json | source!~"console.*" [1d])Configuration
Console interception respects the LOG_LEVEL environment variable:
LOG_LEVEL=debug # Show console.debug() output
LOG_LEVEL=info # Hide console.debug(), show others
LOG_LEVEL=warn # Show only console.warn() and console.error()
LOG_LEVEL=error # Show only console.error()Related Documentation
- Logging Overview - Complete logging system architecture
- Logger Usage Guide - Using the logger directly
- Global Error Handling - Automatic error catching
Summary
Console interception provides automatic structured logging with:
- ✅ Zero Migration: All
console.*calls work unchanged - ✅ JSON Output: Structured logs for Loki/Grafana
- ✅ Context Preserved: Objects, arrays, errors all handled correctly
- ✅ Source Tracking: Know which console method was called
- ✅ Error Details: Stack traces automatically included
- ✅ Production Ready: No code changes needed for production deployment
Just keep using console.* as you always have - it's all logged in beautiful JSON format automatically!