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

526 lines
17 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)
```typescript
// 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):**
```typescript
// 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):**
```typescript
// 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:**
```typescript
// 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`
```typescript
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`
```typescript
export interface ErrorReporter {
report(error: Error, context?: unknown): void;
}
```
### File 3: Enhanced ConsoleLogger (Human-Readable Only)
**Path:** `apps/website/lib/infrastructure/logging/ConsoleLogger.ts`
```typescript
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`
```typescript
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:**
```typescript
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';
}
```
2. **Update handleError:**
```typescript
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);
}
```
3. **Update request method logging:**
```typescript
// 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:**
```typescript
// 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:**
```typescript
// 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:
```bash
# 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! 🎨