Skip to content

Implement request/response logging middleware #11

@koistya

Description

@koistya

Description

Add optional debug logging for HTTP requests and responses to help developers debug OAuth flows. This should be configurable and use structured logging format for easy parsing.

What needs to be done

  1. Add logging configuration to ServerOptions and GetAuthCodeOptions
  2. Implement structured logging for:
    • Incoming HTTP requests (method, URL, headers)
    • Outgoing HTTP responses (status, headers)
    • OAuth flow events (server started, callback received, errors)
    • Timing information (request duration)
  3. Make logging pluggable (console, file, custom logger)
  4. Ensure sensitive data is redacted (tokens, secrets)

Why this matters

OAuth debugging is challenging without visibility:

  • Hard to diagnose redirect issues
  • Difficult to understand provider-specific behaviors
  • No insight into timing issues
  • Can't see what parameters are being sent/received

Good logging enables:

  • Faster debugging of integration issues
  • Better understanding of OAuth flows
  • Performance monitoring
  • Security auditing

Implementation considerations

⚠️ Note: This feature requires critical thinking during implementation. Consider:

  1. Security: How do we ensure tokens and secrets are NEVER logged? Should we have a whitelist of safe parameters?

  2. Performance: Logging can impact performance. Should it be compile-time removable for production?

  3. Alternative approach: Instead of built-in logging, provide hooks/events that users can subscribe to for custom logging

  4. Log levels: Should we support different verbosity levels (DEBUG, INFO, WARN, ERROR)?

  5. Structured vs text: JSON logs are machine-readable but less human-friendly. Which should we prioritize?

Suggested implementation

// Logging types
interface LogEvent {
  timestamp: string;
  level: 'debug' | 'info' | 'warn' | 'error';
  event: string;
  data: Record<string, any>;
  duration?: number;
}

interface Logger {
  log(event: LogEvent): void;
}

// Built-in loggers
class ConsoleLogger implements Logger {
  log(event: LogEvent): void {
    const { timestamp, level, event: eventName, data, duration } = event;
    const message = `[${timestamp}] ${level.toUpperCase()} ${eventName}`;
    
    if (level === 'error') {
      console.error(message, data);
    } else if (level === 'warn') {
      console.warn(message, data);
    } else {
      console.log(message, data);
    }
    
    if (duration !== undefined) {
      console.log(`  Duration: ${duration}ms`);
    }
  }
}

class StructuredLogger implements Logger {
  log(event: LogEvent): void {
    // Output JSON for parsing by log aggregation tools
    console.log(JSON.stringify(event));
  }
}

// Redaction utility
function redactSensitive(obj: any): any {
  const sensitive = ['access_token', 'refresh_token', 'client_secret', 'code'];
  
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }
  
  const result: any = Array.isArray(obj) ? [] : {};
  
  for (const [key, value] of Object.entries(obj)) {
    if (sensitive.includes(key.toLowerCase())) {
      result[key] = '[REDACTED]';
    } else if (typeof value === 'object') {
      result[key] = redactSensitive(value);
    } else {
      result[key] = value;
    }
  }
  
  return result;
}

// Updated ServerOptions
interface ServerOptions {
  // ... existing options
  logger?: Logger;
  logLevel?: 'debug' | 'info' | 'warn' | 'error';
  logRequests?: boolean;
  logResponses?: boolean;
  logTimings?: boolean;
}

// Usage in server implementation
private handleRequest(request: Request): Response {
  const startTime = performance.now();
  
  if (this.logger && this.logRequests) {
    this.logger.log({
      timestamp: new Date().toISOString(),
      level: 'debug',
      event: 'http.request',
      data: redactSensitive({
        method: request.method,
        url: request.url,
        headers: Object.fromEntries(request.headers.entries()),
        ip: request.headers.get('x-forwarded-for') || 'localhost'
      })
    });
  }
  
  // ... handle request
  
  const response = new Response(...);
  
  if (this.logger && this.logResponses) {
    const duration = performance.now() - startTime;
    this.logger.log({
      timestamp: new Date().toISOString(),
      level: 'debug',
      event: 'http.response',
      data: redactSensitive({
        status: response.status,
        headers: Object.fromEntries(response.headers.entries())
      }),
      duration
    });
  }
  
  return response;
}

Usage examples

// Basic console logging
await getAuthCode({
  authorizationUrl: "...",
  logger: new ConsoleLogger(),
  logLevel: 'debug',
  logRequests: true,
  logResponses: true,
  logTimings: true
});

// Custom logger
class CustomLogger implements Logger {
  log(event: LogEvent): void {
    // Send to logging service
    fetch('https://logs.example.com', {
      method: 'POST',
      body: JSON.stringify(event)
    });
  }
}

// Production-safe logging (no sensitive data)
await getAuthCode({
  authorizationUrl: "...",
  logger: new StructuredLogger(),
  logLevel: 'info' // Only important events
});

Testing requirements

  • Verify sensitive data is redacted
  • Test all log levels
  • Verify performance impact is minimal
  • Test custom logger integration
  • Ensure logs are useful for debugging

Skills required

  • TypeScript
  • Logging best practices
  • Security awareness (data redaction)
  • Performance optimization
  • Structured logging concepts

Difficulty

Easy - Straightforward implementation with important security considerations

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions