Files
gridpilot.gg/plans/UNIFIED_LOGGING_PLAN.md
2026-01-06 13:21:55 +01:00

17 KiB
Raw Blame History

Unified Logging Plan - Professional & Developer Friendly

Problem Summary

Current Issues:

  • Website logs are overly aggressive and verbose
  • Network errors show full stack traces (looks like syntax errors)
  • Multiple error formats for same issue
  • Not machine-readable
  • Different patterns than apps/api

Goal: Create unified, professional logging that's both machine-readable AND beautiful for developers.

Solution Overview

1. Unified Logger Interface (No Core Imports)

// apps/website/lib/interfaces/Logger.ts
export interface Logger {
  debug(message: string, context?: unknown): void;
  info(message: string, context?: unknown): void;
  warn(message: string, context?: unknown): void;
  error(message: string, error?: Error, context?: unknown): void;
}

2. How Website Logging Aligns with apps/api

apps/api ConsoleLogger (Simple & Clean):

// adapters/logging/ConsoleLogger.ts
formatMessage(level: string, message: string, context?: unknown): string {
  const timestamp = new Date().toISOString();
  const contextStr = context ? ` | ${JSON.stringify(context)}` : '';
  return `[${timestamp}] ${level.toUpperCase()}: ${message}${contextStr}`;
}

// Output: [2026-01-06T12:00:00.000Z] WARN: Network error, retrying... | {"endpoint":"/auth/session"}

apps/website ConsoleLogger (Enhanced & Developer-Friendly):

// apps/website/lib/infrastructure/logging/ConsoleLogger.ts
formatOutput(level: string, source: string, message: string, context?: unknown, error?: Error): void {
  const color = this.COLORS[level];
  const emoji = this.EMOJIS[level];
  const prefix = this.PREFIXES[level];
  
  // Same core format as apps/api, but enhanced with colors/emojis
  console.groupCollapsed(`%c${emoji} [${source.toUpperCase()}] ${prefix}: ${message}`, `color: ${color}; font-weight: bold;`);
  console.log(`%cTimestamp:`, 'color: #666; font-weight: bold;', new Date().toISOString());
  console.log(`%cContext:`, 'color: #666; font-weight: bold;');
  console.dir(context, { depth: 3, colors: true });
  console.groupEnd();
}

// Output: ⚠️ [API] NETWORK WARN: Network error, retrying...
//         ├─ Timestamp: 2026-01-06T12:00:00.000Z
//         ├─ Context: { endpoint: "/auth/session", ... }

Alignment:

  • Same timestamp format (ISO 8601)
  • Same log levels (debug, info, warn, error)
  • Same context structure
  • Same message patterns
  • Website adds colors/emojis for better UX

3. Unified API Client Logging Strategy

Both apps/api and apps/website use the same patterns:

// In BaseApiClient (shared logic):
private handleError(error: ApiError): void {
  const severity = error.getSeverity();
  const message = error.getDeveloperMessage();
  
  // Same logic for both:
  if (error.context.isRetryable && error.context.retryCount > 0) {
    // Network errors during retry = warn (not error)
    this.logger.warn(message, error.context);
  } else if (severity === 'error') {
    // Final failure = error
    this.logger.error(message, error, error.context);
  } else {
    // Other errors = warn
    this.logger.warn(message, error.context);
  }
}

4. Unified Error Classification

Both environments use the same severity levels:

  • error: Critical failures (server down, auth failures, data corruption)
  • warn: Expected errors (network timeouts, CORS, validation failures)
  • info: Normal operations (successful retries, connectivity恢复)
  • debug: Detailed info (development only)

5. Example: Same Error, Different Output

Scenario: Server down, retrying connection

apps/api output:

[2026-01-06T12:00:00.000Z] WARN: [NETWORK_ERROR] GET /auth/session retry:1 | {"endpoint":"/auth/session","method":"GET","retryCount":1}

apps/website output:

⚠️ [API] NETWORK WARN: GET /auth/session - retry:1
├─ Timestamp: 2026-01-06T12:00:00.000Z
├─ Endpoint: /auth/session
├─ Method: GET
├─ Retry Count: 1
└─ Hint: Check if API server is running and CORS is configured

Key Alignment Points:

  1. Same log level: warn (not error)
  2. Same context: {endpoint, method, retryCount}
  3. Same message pattern: Includes retry count
  4. Same timestamp format: ISO 8601
  5. Website just adds: Colors, emojis, and developer hints

This creates a unified logging ecosystem where:

  • Logs can be parsed the same way
  • Severity levels mean the same thing
  • Context structures are identical
  • Website enhances for developer experience
  • apps/api keeps it simple for server logs

Implementation Files

File 1: Logger Interface

Path: apps/website/lib/interfaces/Logger.ts

export interface Logger {
  debug(message: string, context?: unknown): void;
  info(message: string, context?: unknown): void;
  warn(message: string, context?: unknown): void;
  error(message: string, error?: Error, context?: unknown): void;
}

File 2: ErrorReporter Interface

Path: apps/website/lib/interfaces/ErrorReporter.ts

export interface ErrorReporter {
  report(error: Error, context?: unknown): void;
}

File 3: Enhanced ConsoleLogger (Human-Readable Only)

Path: apps/website/lib/infrastructure/logging/ConsoleLogger.ts

import { Logger } from '../../interfaces/Logger';

export class ConsoleLogger implements Logger {
  private readonly COLORS = { debug: '#888888', info: '#00aaff', warn: '#ffaa00', error: '#ff4444' };
  private readonly EMOJIS = { debug: '🐛', info: '', warn: '⚠️', error: '❌' };
  private readonly PREFIXES = { debug: 'DEBUG', info: 'INFO', warn: 'WARN', error: 'ERROR' };

  private shouldLog(level: string): boolean {
    if (process.env.NODE_ENV === 'test') return level === 'error';
    if (process.env.NODE_ENV === 'production') return level !== 'debug';
    return true;
  }

  private formatOutput(level: string, source: string, message: string, context?: unknown, error?: Error): void {
    const color = this.COLORS[level];
    const emoji = this.EMOJIS[level];
    const prefix = this.PREFIXES[level];
    
    console.groupCollapsed(`%c${emoji} [${source.toUpperCase()}] ${prefix}: ${message}`, `color: ${color}; font-weight: bold;`);
    
    console.log(`%cTimestamp:`, 'color: #666; font-weight: bold;', new Date().toISOString());
    console.log(`%cSource:`, 'color: #666; font-weight: bold;', source);
    
    if (context) {
      console.log(`%cContext:`, 'color: #666; font-weight: bold;');
      console.dir(context, { depth: 3, colors: true });
    }
    
    if (error) {
      console.log(`%cError Details:`, 'color: #666; font-weight: bold;');
      console.log(`%cType:`, 'color: #ff4444; font-weight: bold;', error.name);
      console.log(`%cMessage:`, 'color: #ff4444; font-weight: bold;', error.message);
      
      if (process.env.NODE_ENV === 'development' && error.stack) {
        console.log(`%cStack Trace:`, 'color: #666; font-weight: bold;');
        console.log(error.stack);
      }
    }
    
    console.groupEnd();
  }

  debug(message: string, context?: unknown): void {
    if (!this.shouldLog('debug')) return;
    this.formatOutput('debug', 'website', message, context);
  }

  info(message: string, context?: unknown): void {
    if (!this.shouldLog('info')) return;
    this.formatOutput('info', 'website', message, context);
  }

  warn(message: string, context?: unknown): void {
    if (!this.shouldLog('warn')) return;
    this.formatOutput('warn', 'website', message, context);
  }

  error(message: string, error?: Error, context?: unknown): void {
    if (!this.shouldLog('error')) return;
    this.formatOutput('error', 'website', message, context, error);
  }
}

File 4: ConsoleErrorReporter

Path: apps/website/lib/infrastructure/ConsoleErrorReporter.ts

import { ErrorReporter } from '../../interfaces/ErrorReporter';

export class ConsoleErrorReporter implements ErrorReporter {
  report(error: Error, context?: unknown): void {
    const timestamp = new Date().toISOString();
    console.error(`[${timestamp}] Error reported:`, error.message, { error, context });
  }
}

File 5: Updated BaseApiClient

Path: apps/website/lib/api/base/BaseApiClient.ts

Key Changes:

  1. Update createNetworkError:
private createNetworkError(error: Error, method: string, path: string, retryCount: number = 0): ApiError {
  // ... existing logic ...
  
  return new ApiError(
    message,
    errorType,
    {
      endpoint: path,
      method,
      timestamp: new Date().toISOString(),
      retryCount,
      troubleshooting: this.getTroubleshootingContext(error, path),
      isRetryable: retryableTypes.includes(errorType),
      isConnectivity: errorType === 'NETWORK_ERROR' || errorType === 'TIMEOUT_ERROR',
      developerHint: this.getDeveloperHint(error, path, method),
    },
    error
  );
}

private getDeveloperHint(error: Error, path: string, method: string): string {
  if (error.message.includes('fetch failed') || error.message.includes('Failed to fetch')) {
    return 'Check if API server is running and CORS is configured correctly';
  }
  if (error.message.includes('timeout')) {
    return 'Request timed out - consider increasing timeout or checking network';
  }
  if (error.message.includes('ECONNREFUSED')) {
    return 'Connection refused - verify API server address and port';
  }
  return 'Review network connection and API endpoint configuration';
}
  1. Update handleError:
private handleError(error: ApiError): void {
  const severity = error.getSeverity();
  const message = error.getDeveloperMessage();
  
  const enhancedContext = {
    ...error.context,
    severity,
    isRetryable: error.isRetryable(),
    isConnectivity: error.isConnectivityIssue(),
  };

  if (severity === 'error') {
    this.logger.error(message, error, enhancedContext);
  } else if (severity === 'warn') {
    this.logger.warn(message, enhancedContext);
  } else {
    this.logger.info(message, enhancedContext);
  }

  this.errorReporter.report(error, enhancedContext);
}
  1. Update request method logging:
// In catch block:
} catch (error) {
  const responseTime = Date.now() - startTime;

  if (error instanceof ApiError) {
    // Reduce verbosity - only log final failure
    if (process.env.NODE_ENV === 'development' && requestId) {
      try {
        const apiLogger = getGlobalApiLogger();
        // This will use warn level for retryable errors
        apiLogger.logError(requestId, error, responseTime);
      } catch (e) {
        // Silent fail
      }
    }
    throw error;
  }
  // ... rest of error handling
}

File 6: Updated ApiRequestLogger

Path: apps/website/lib/infrastructure/ApiRequestLogger.ts

Key Changes:

// Update logError to use warn for network errors:
logError(id: string, error: Error, duration: number): void {
  // ... existing setup ...
  
  const isNetworkError = error.message.includes('fetch') ||
                        error.message.includes('Failed to fetch') ||
                        error.message.includes('NetworkError');

  if (this.options.logToConsole) {
    const emoji = isNetworkError ? '⚠️' : '❌';
    const prefix = isNetworkError ? 'NETWORK WARN' : 'ERROR';
    const color = isNetworkError ? '#ffaa00' : '#ff4444';

    console.groupCollapsed(
      `%c${emoji} [API] ${prefix}: ${log.method} ${log.url}`,
      `color: ${color}; font-weight: bold; font-size: 12px;`
    );
    console.log(`%cRequest ID:`, 'color: #666; font-weight: bold;', id);
    console.log(`%cDuration:`, 'color: #666; font-weight: bold;', `${duration.toFixed(2)}ms`);
    console.log(`%cError:`, 'color: #666; font-weight: bold;', error.message);
    console.log(`%cType:`, 'color: #666; font-weight: bold;', error.name);
    
    if (process.env.NODE_ENV === 'development' && error.stack) {
      console.log(`%cStack:`, 'color: #666; font-weight: bold;');
      console.log(error.stack);
    }
    console.groupEnd();
  }

  // Don't report network errors to external services
  if (!isNetworkError) {
    const globalHandler = getGlobalErrorHandler();
    globalHandler.report(error, {
      source: 'api_request',
      url: log.url,
      method: log.method,
      duration,
      requestId: id,
    });
  }
}

File 7: Updated GlobalErrorHandler

Path: apps/website/lib/infrastructure/GlobalErrorHandler.ts

Key Changes:

// In handleWindowError:
private handleWindowError = (event: ErrorEvent): void => {
  const error = event.error;
  
  // Check if this is a network/CORS error (expected in some cases)
  if (error instanceof TypeError && error.message.includes('fetch')) {
    this.logger.warn('Network error detected', {
      type: 'network_error',
      message: error.message,
      url: event.filename
    });
    return; // Don't prevent default for network errors
  }
  
  // ... existing logic for other errors ...
};

// Update logErrorWithMaximumDetail:
private logErrorWithMaximumDetail(error: Error | ApiError, context: Record<string, unknown>): void {
  if (!this.options.verboseLogging) return;

  const isApiError = error instanceof ApiError;
  const isWarning = isApiError && error.getSeverity() === 'warn';

  if (isWarning) {
    console.groupCollapsed(`%c⚠ [WARNING] ${error.message}`, 'color: #ffaa00; font-weight: bold; font-size: 14px;');
    console.log('Context:', context);
    console.groupEnd();
    return;
  }

  // Full error details for actual errors
  console.groupCollapsed(`%c❌ [ERROR] ${error.name || 'Error'}: ${error.message}`, 'color: #ff4444; font-weight: bold; font-size: 16px;');
  
  console.log('%cError Details:', 'color: #ff4444; font-weight: bold; font-size: 14px;');
  console.table({
    Name: error.name,
    Message: error.message,
    Type: isApiError ? error.type : 'N/A',
    Severity: isApiError ? error.getSeverity() : 'error',
    Retryable: isApiError ? error.isRetryable() : 'N/A',
  });

  console.log('%cContext:', 'color: #666; font-weight: bold; font-size: 14px;');
  console.dir(context, { depth: 4, colors: true });

  if (process.env.NODE_ENV === 'development' && error.stack) {
    console.log('%cStack Trace:', 'color: #666; font-weight: bold; font-size: 14px;');
    console.log(error.stack);
  }

  if (isApiError && error.context?.developerHint) {
    console.log('%c💡 Developer Hint:', 'color: #00aaff; font-weight: bold; font-size: 14px;');
    console.log(error.context.developerHint);
  }

  console.groupEnd();
  this.logger.error(error.message, error, context);
}

Expected Results

Before (Current - Too Verbose)

[API-ERROR] [NETWORK_ERROR] Unable to connect to server. Possible CORS or network issue. GET /auth/session {
  error: Error [ApiError]: Unable to connect to server. Possible CORS or network issue.
      at AuthApiClient.createNetworkError (lib/api/base/BaseApiClient.ts:136:12)
      at executeRequest (lib/api/base/BaseApiClient.ts:314:31)
      ...
}
[USER-NOTIFICATION] Unable to connect to the server. Please check your internet connection.
[API ERROR] GET http://api:3000/auth/session
  Request ID: req_1767694969495_4
  Duration: 8.00ms
  Error: Unable to connect to server. Possible CORS or network issue.
  Type: ApiError
  Stack: ApiError: Unable to connect to server...

After (Unified - Clean & Beautiful)

Beautiful console output:

⚠️ [API] NETWORK WARN: GET /auth/session - retry:1
├─ Request ID: req_123
├─ Duration: 8.00ms
├─ Error: fetch failed
├─ Type: TypeError
└─ Hint: Check if API server is running and CORS is configured

And user notification (separate):

[USER-NOTIFICATION] Unable to connect to the server. Please check your internet connection.

Benefits

Developer-Friendly: Beautiful colors, emojis, and formatting Reduced Noise: Appropriate severity levels prevent spam Unified Format: Same patterns as apps/api No Core Imports: Website remains independent Professional: Industry-standard logging practices Clean Output: Human-readable only, no JSON clutter

Testing Checklist

  1. Server Down: Should show warn level, no stack trace, retry attempts
  2. CORS Error: Should show warn level with troubleshooting hint
  3. Auth Error (401): Should show warn level, retryable
  4. Server Error (500): Should show error level with full details
  5. Validation Error (400): Should show warn level, not retryable
  6. Successful Call: Should show info level with duration

Quick Implementation

Run these commands to implement:

# 1. Create interfaces
mkdir -p apps/website/lib/interfaces
cat > apps/website/lib/interfaces/Logger.ts << 'EOF'
export interface Logger {
  debug(message: string, context?: unknown): void;
  info(message: string, context?: unknown): void;
  warn(message: string, context?: unknown): void;
  error(message: string, error?: Error, context?: unknown): void;
}
EOF

# 2. Create ErrorReporter interface
cat > apps/website/lib/interfaces/ErrorReporter.ts << 'EOF'
export interface ErrorReporter {
  report(error: Error, context?: unknown): void;
}
EOF

# 3. Update ConsoleLogger (use the enhanced version above)
# 4. Update ConsoleErrorReporter (use the version above)
# 5. Update BaseApiClient (use the changes above)
# 6. Update ApiRequestLogger (use the changes above)
# 7. Update GlobalErrorHandler (use the changes above)

This single plan provides everything needed to transform the logging from verbose and confusing to professional and beautiful! 🎨