Unified JSON logging across Next.js, Python, and Firebase Functions
Establish unified, machine-readable JSON logging across all platforms for centralized aggregation, powerful querying, and correlation with distributed traces.
Problem with traditional logging:
console.log('User ' + userId + ' logged in from ' + ip)
Structured logging solution:
logger.info('User logged in', {
user_id: userId,
ip_address: ip,
auth_method: 'oauth'
})
Output (JSON):
{
"level": "info",
"timestamp": "2024-01-15T10:30:00.000Z",
"message": "User logged in",
"user_id": "user_123",
"ip_address": "192.168.1.1",
"auth_method": "oauth",
"trace_id": "abc123...",
"service": "api"
}
Benefits:
user_id:user_123 AND level:errorUse consistent field names across all platforms:
| Field | Type | Description | Example |
|---|---|---|---|
| level | string/number | Log severity | "info", 30 |
| timestamp | ISO 8601 | When event occurred | "2024-01-15T10:30:00.000Z" |
| message | string | Human-readable message | "User logged in" |
| service.name | string | Service identifier | "api", "web", "worker" |
| service.version | string | Deployment version | "1.2.3" |
| environment | string | Runtime environment | "production", "staging" |
| trace_id | string | OpenTelemetry trace ID | "abc123..." |
| span_id | string | OpenTelemetry span ID | "def456..." |
| user_id | string | User identifier | "user_789" |
| request_id | string | Request identifier | "req_xyz" |
{
"http": {
"method": "POST",
"url": "/api/users",
"status_code": 201,
"user_agent": "Mozilla/5.0..."
},
"error": {
"type": "ValidationError",
"message": "Invalid email",
"stack": "Error: Invalid email\n at..."
}
}
Pino is the fastest JSON logger for Node.js.
// lib/logger.ts
import pino from 'pino'
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
// Pretty print in development
transport: process.env.NODE_ENV === 'development'
? { target: 'pino-pretty', options: { colorize: true } }
: undefined,
// Base fields included in every log
base: {
service: {
name: process.env.SERVICE_NAME || 'api',
version: process.env.SERVICE_VERSION || '1.0.0'
},
environment: process.env.NODE_ENV || 'development'
},
// Redact sensitive fields
redact: {
paths: ['password', 'api_key', 'credit_card', '*.password', '*.token'],
censor: '[REDACTED]'
}
})
export default logger
import logger from '@/lib/logger'
// Simple message
logger.info('Server started')
// With context
logger.info({ user_id: '123', action: 'login' }, 'User logged in')
// Child logger (adds context to all subsequent logs)
const requestLogger = logger.child({ request_id: req.id })
requestLogger.info('Processing request')
requestLogger.error({ err }, 'Request failed')
// lib/logger-middleware.ts
import { NextRequest, NextResponse } from 'next/server'
import logger from '@/lib/logger'
export function withLogging(
handler: (req: NextRequest) => Promise<NextResponse>
) {
return async (req: NextRequest) => {
const requestLogger = logger.child({
request_id: crypto.randomUUID(),
http: {
method: req.method,
url: req.url
}
})
requestLogger.info('Request started')
try {
const response = await handler(req)
requestLogger.info({ status: response.status }, 'Request completed')
return response
} catch (error) {
requestLogger.error({ err: error }, 'Request failed')
throw error
}
}
}
// Usage in API route
export const GET = withLogging(async (req) => {
// Your handler logic
})
// lib/logger.ts (production)
import pino from 'pino'
const logger = pino({
level: 'info',
// Don't use pino-pretty in production (performance)
transport: process.env.NODE_ENV === 'production'
? {
target: 'pino/file',
options: { destination: 1 } // stdout
}
: { target: 'pino-pretty' },
// Async logging (don't block event loop)
destination: pino.destination({
sync: false // Async mode
})
})
export default logger
structlog integrates with Python's standard logging library.
# logging_config.py
import structlog
import logging
import sys
def configure_logging():
# Configure standard library logging
logging.basicConfig(
format="%(message)s",
stream=sys.stdout,
level=logging.INFO,
)
# Configure structlog
structlog.configure(
processors=[
# Add log level
structlog.stdlib.add_log_level,
# Add timestamp
structlog.processors.TimeStamper(fmt="iso"),
# Add calling location (file:line)
structlog.processors.CallsiteParameterAdder(
parameters=[
structlog.processors.CallsiteParameter.FILENAME,
structlog.processors.CallsiteParameter.LINENO,
]
),
# Format exceptions
structlog.processors.format_exc_info,
# Render as JSON
structlog.processors.JSONRenderer()
],
wrapper_class=structlog.stdlib.BoundLogger,
context_class=dict,
logger_factory=structlog.stdlib.LoggerFactory(),
cache_logger_on_first_use=True,
)
# Call at app startup
configure_logging()
import structlog
logger = structlog.get_logger()
# Simple message
logger.info("server_started", port=8000)
# With context
logger.info(
"user_logged_in",
user_id="user_123",
auth_method="oauth"
)
# Error logging