PatternsObservability
Logging Strategies (Structured Logging)
Implement effective logging for debugging and monitoring
Structured logging makes logs searchable, filterable, and actionable. Essential for debugging production issues.
Structured Logging
// ❌ BAD: Unstructured logs
console.log('User logged in: john@example.com');
// ✅ GOOD: Structured logs
logger.info('user_logged_in', {
userId: '123',
email: 'john@example.com',
timestamp: Date.now(),
sessionId: 'abc',
});Logger Implementation
// utils/logger.ts
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
interface LogEntry {
level: LogLevel;
message: string;
context?: Record<string, any>;
timestamp: number;
userId?: string;
sessionId?: string;
}
class Logger {
private level: LogLevel = 'info';
private context: Record<string, any> = {};
setLevel(level: LogLevel) {
this.level = level;
}
setContext(context: Record<string, any>) {
this.context = { ...this.context, ...context };
}
private log(level: LogLevel, message: string, context?: Record<string, any>) {
const entry: LogEntry = {
level,
message,
context: { ...this.context, ...context },
timestamp: Date.now(),
};
// Console output (dev)
if (process.env.NODE_ENV === 'development') {
console[level](message, entry.context);
}
// Send to logging service (production)
if (process.env.NODE_ENV === 'production') {
this.sendToService(entry);
}
}
debug(message: string, context?: Record<string, any>) {
if (this.shouldLog('debug')) {
this.log('debug', message, context);
}
}
info(message: string, context?: Record<string, any>) {
if (this.shouldLog('info')) {
this.log('info', message, context);
}
}
warn(message: string, context?: Record<string, any>) {
if (this.shouldLog('warn')) {
this.log('warn', message, context);
}
}
error(message: string, context?: Record<string, any>) {
this.log('error', message, context);
}
private shouldLog(level: LogLevel): boolean {
const levels = ['debug', 'info', 'warn', 'error'];
return levels.indexOf(level) >= levels.indexOf(this.level);
}
private sendToService(entry: LogEntry) {
// Send to Sentry, DataDog, etc.
fetch('/api/logs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(entry),
}).catch(() => {
// Silently fail to avoid infinite loops
});
}
}
export const logger = new Logger();Usage Patterns
API Call Logging
export async function apiRequest(url: string, options?: RequestInit) {
const requestId = crypto.randomUUID();
logger.info('api_request_start', {
requestId,
url,
method: options?.method || 'GET',
});
const start = performance.now();
try {
const response = await fetch(url, options);
const duration = performance.now() - start;
logger.info('api_request_success', {
requestId,
url,
status: response.status,
duration,
});
return response;
} catch (error) {
const duration = performance.now() - start;
logger.error('api_request_error', {
requestId,
url,
error: error instanceof Error ? error.message : 'Unknown error',
duration,
});
throw error;
}
}User Action Logging
export function useActionLogger() {
const logAction = (action: string, context?: Record<string, any>) => {
logger.info('user_action', {
action,
...context,
pathname: window.location.pathname,
referrer: document.referrer,
});
};
return { logAction };
}
// Usage
function Component() {
const { logAction } = useActionLogger();
const handleClick = () => {
logAction('button_clicked', {
buttonId: 'submit',
formData: { ... },
});
};
return <button onClick={handleClick}>Submit</button>;
}Error Logging
// Global error handler
window.addEventListener('error', (event) => {
logger.error('unhandled_error', {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack,
});
});
// Promise rejection handler
window.addEventListener('unhandledrejection', (event) => {
logger.error('unhandled_rejection', {
reason: event.reason,
promise: event.promise,
});
});
// React Error Boundary
class ErrorBoundary extends React.Component {
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
logger.error('react_error', {
error: error.toString(),
componentStack: errorInfo.componentStack,
});
}
}Performance Logging
export function logPerformance() {
// Core Web Vitals
onLCP((metric) => {
logger.info('metric_lcp', { value: metric.value });
});
onFID((metric) => {
logger.info('metric_fid', { value: metric.value });
});
onCLS((metric) => {
logger.info('metric_cls', { value: metric.value });
});
// Custom timing
const navEntry = performance.getEntriesByType('navigation')[0] as any;
logger.info('page_timing', {
dns: navEntry.domainLookupEnd - navEntry.domainLookupStart,
tcp: navEntry.connectEnd - navEntry.connectStart,
ttfb: navEntry.responseStart - navEntry.requestStart,
domLoad: navEntry.domContentLoadedEventEnd - navEntry.fetchStart,
pageLoad: navEntry.loadEventEnd - navEntry.fetchStart,
});
}Log Sampling
// Sample logs in high-traffic apps
class SampledLogger extends Logger {
private sampleRate = 0.1; // 10%
setSampleRate(rate: number) {
this.sampleRate = rate;
}
protected shouldSample(): boolean {
return Math.random() < this.sampleRate;
}
info(message: string, context?: Record<string, any>) {
if (this.shouldSample()) {
super.info(message, context);
}
}
// Always log errors
error(message: string, context?: Record<string, any>) {
super.error(message, context);
}
}Best Practices
- Structured format - JSON, searchable
- Contextual info - userId, sessionId, route
- Log levels - debug, info, warn, error
- Sample in production - Don't log everything
- Never log PII - Passwords, credit cards
- Correlation IDs - Track requests across services
- Error stack traces - Full context
- Performance metrics - Track slowness
- Centralized logging - Single source of truth
- Alerts on errors - Be proactive
Structured logging enables effective debugging—log smartly with context and levels!