Skip to content

Watch out! You're reading a series of articles

Logging Levels: The Good, The Bad, and The "Why Is This in Production?"

Production was down, and I was staring at logs that looked like this:

typescript
console.log("Starting process...")
console.log("Done!")

Somewhere between "Starting" and "Done", our payment processing system had failed, affecting thousands of users, and I had absolutely no idea why. That's when I learned the hard way about the importance of proper logging levels.

The Five Levels of Logging Enlightenment

Think of log levels like spice levels at a Thai restaurant - you need the right amount for the right situation. Let's break them down with some real TypeScript examples:

1. DEBUG: The "I'm Just Curious" Level

typescript
class PaymentProcessor {
  async processPayment(payment: Payment) {
    logger.debug(`Starting payment processing for ID: ${payment.id}`, {
      amount: payment.amount,
      currency: payment.currency,
      timestamp: new Date().toISOString()
    });
    
    // Processing logic here...
    
    logger.debug('Payment validation steps completed', {
      validationResults: results,
      processingTime: elapsed
    });
  }
}

Use DEBUG when you're in detective mode. It's like those behind-the-scenes DVD extras - interesting during development, but you don't need them in the final cut.

2. INFO: The "Everything's Fine" Level

typescript
class UserAuthService {
  async login(username: string) {
    logger.info('User login successful', {
      user: username,
      loginTime: new Date().toISOString(),
      ipAddress: request.ip
    });
  }
}

INFO is for tracking normal operations. It's like those "Your package has been delivered" notifications - useful to know, but not something to wake you up at night for.

3. WARNING: The "Hmm, That's Weird" Level

typescript
class DatabaseConnection {
  async query(sql: string) {
    try {
      const result = await this.execute(sql);
      if (this.connectionPool.available < 3) {
        logger.warn('Database connection pool running low', {
          availableConnections: this.connectionPool.available,
          totalConnections: this.connectionPool.total,
          sql: sql.substring(0, 100) // Don't log entire queries!
        });
      }
      return result;
    } catch (error) {
      // More severe error handling...
    }
  }
}

WARN is for those "Check engine" light moments - something's not quite right, but your car hasn't exploded... yet.

4. ERROR: The "Houston, We Have a Problem" Level

typescript
class PaymentGateway {
  async processTransaction(transaction: Transaction) {
    try {
      return await this.gateway.charge(transaction);
    } catch (error) {
      logger.error('Payment processing failed', {
        transactionId: transaction.id,
        error: error.message,
        errorCode: error.code,
        stackTrace: error.stack,
        // Never log full card details!
        lastFourDigits: transaction.card.lastFour
      });
      throw error;
    }
  }
}

ERROR is for when things actually break. Like when your code tries to divide by zero, or when that "definitely tested" feature meets real users.

5. CRITICAL: The "Wake Everyone Up" Level

typescript
class SystemMonitor {
  checkDiskSpace() {
    const freeSpace = this.getDiskSpace();
    if (freeSpace.percentage < 1) {
      logger.critical('SYSTEM CRITICAL: Disk space nearly exhausted', {
        freeSpacePercent: freeSpace.percentage,
        freeSpaceBytes: freeSpace.bytes,
        timestamp: new Date().toISOString(),
        affectedServices: this.getAffectedServices()
      });
      // Initiate emergency cleanup or notification
    }
  }
}

CRITICAL is for those "wake up the CTO" moments. Like when your disk is full, your database is down, or someone deployed straight to production on a Friday afternoon.

Real Talk: When to Use What

Here's my practical guide after years of 3 AM debugging sessions:

  • DEBUG: Use it liberally in development, but sparingly in production. It's like developer commentary - fascinating for you, boring for everyone else.

  • INFO: Perfect for tracking business events and flow. Want to know how many users logged in today? INFO is your friend.

  • WARNING: Use for "soft" errors and potential issues. Like when that API you're calling is getting a bit slow, or when someone tries to log in with "password123".

  • ERROR: For all those "this shouldn't happen" moments that, inevitably, do happen. If you need to fix something, it should be an ERROR.

  • CRITICAL: Reserve this for the real disasters. If you're logging more than a few CRITICAL events per month, you're either being too dramatic or you have bigger problems to solve.

The Golden Rules of Log Levels

  1. Be Consistent: If a failed payment is an ERROR, it should always be an ERROR. Don't play log level roulette.

  2. Context is King: Don't just log "Error occurred" - include enough context to understand what happened without having to reproduce the issue.

  3. Think About Scale: What's useful when debugging locally might be overwhelming in production. Use log levels to control the noise.

Remember that one time I mentioned the 2 AM debugging session? Well, after implementing proper log levels, a similar issue occurred, but this time our logs told us exactly what went wrong:

typescript
[2024-01-15T02:00:01.123Z] ERROR Payment processing failed {
  "transactionId": "tx_123",
  "errorCode": "GATEWAY_TIMEOUT",
  "retryCount": 3,
  "gatewayResponse": "Connection refused"
}

Five minutes later, we had identified the issue (a gateway timeout), fixed it (increased the timeout threshold), and I was back in bed. That's the power of proper log levels.

Conclusion

Logging levels aren't just about categorizing messages - they're about telling a story that helps you understand what your application is doing, especially when things go wrong. Use them wisely, and your future self (probably at 2 AM) will thank you.

Watch out! You're reading a series of articles