Skip to content

Log Levels: Choosing the Right Level for Every Message

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Log Levels: Choosing the Right Level for Every Message. We cover key concepts, practical examples, and best practices to help you master this topic.

Log levels categorize log entries by severity, enabling filtering and alerting. The standard levels are error, warn, info, debug, and trace. Choosing the right level for each message is critical: too few logs make debugging impossible, too many create noise and increase costs.

flowchart LR
    ERROR -->|Fatal errors requiring immediate attention| Alert[PagerDuty Alert]
    WARN -->|Unexpected but handled| WarnDashboard[Dashboard Warning]
    INFO -->|Significant events| InfoDashboard[Operations Dashboard]
    DEBUG -->|Detailed diagnostics| DebugOn[Enabled on demand]
    TRACE -->|Step-by-step execution| TraceOn[Enabled for specific traces]

What You'll Learn

  • When to use each log level
  • Dynamic log level configuration at runtime
  • Log sampling for high-volume debug logs
  • Log noise reduction strategies

Why It Matters

Using the wrong log level has real consequences: logging too much at INFO in production increases costs by 10x and buries real issues. Too many ERROR-level logs cause alert fatigue where real emergencies are ignored.

Real-World Use

A payment service uses ERROR for failed transactions (triggers PagerDuty), WARN for declined payments (business as usual), INFO for significant lifecycle events (user registered), DEBUG for database queries (enabled on-demand for debugging), and TRACE for step-by-step processing (sampled 1%).

Log Level Implementation

Log Level Configuration

class LogLevelManager {
  constructor(defaultLevel = 'info') {
    this.levels = { error: 0, warn: 1, info: 2, debug: 3, trace: 4 };
    this.currentLevel = this.levels[defaultLevel];
    this.overrides = new Map(); // Per-module level overrides
  }

  setLevel(level) {
    if (!this.levels.hasOwnProperty(level)) {
      throw new Error(`Invalid log level: ${level}`);
    }
    this.currentLevel = this.levels[level];
  }

  setModuleLevel(moduleName, level) {
    this.overrides.set(moduleName, this.levels[level]);
  }

  shouldLog(level, moduleName) {
    const effectiveLevel = this.overrides.get(moduleName) ?? this.currentLevel;
    return this.levels[level] <= effectiveLevel;
  }
}

Expected output:

Default level: info. Setting level to debug enables debug and trace. Per-module override allows payment-service to log at trace while others stay at info.

Dynamic Log Level Endpoint

const logManager = new LogLevelManager(process.env.LOG_LEVEL || 'info');

// Security: authenticated endpoint for changing log levels
app.post('/api/admin/log-level', authenticateAdmin, (req, res) => {
  const { level, module: moduleName, duration } = req.body;

  if (moduleName) {
    logManager.setModuleLevel(moduleName, level);
  } else {
    logManager.setLevel(level);
  }

  // Auto-reset after duration (for temporary debugging)
  if (duration) {
    const originalLevel = logManager.currentLevel;
    setTimeout(() => {
      logManager.setLevel('info');
      logger.info({ previousLevel: originalLevel }, 'Log level auto-reset to info');
    }, duration);
  }

  logger.warn({ level, module: moduleName, setBy: req.user.id }, 'Log level changed');
  res.json({ level, module: moduleName, duration });
});

Expected output:

POST /api/admin/log-level with {level: "debug", duration: 300000} enables debug logs for 5 minutes, then auto-resets to info.

Log Sampling for High-Volume Debug

class SampledLogger {
  constructor(logger, sampleRate = 0.01) {
    this.logger = logger;
    this.sampleRate = sampleRate;
    this.counters = new Map();
  }

  debug(message, context = {}) {
    if (Math.random() < this.sampleRate) {
      this.logger.debug({ ...context, sampled: true, sampleRate: this.sampleRate }, message);
    }
  }

  // Rate-limited debug: log first N occurrences, then every Nth
  rateLimitedDebug(key, message, context = {}, options = {}) {
    const { maxBurst = 10, interval = 100 } = options;
    const counter = this.counters.get(key) || { count: 0, lastLog: 0 };
    counter.count++;

    if (counter.count <= maxBurst) {
      this.logger.debug({ ...context, rateLimited: true, occurrence: counter.count }, message);
      counter.lastLog = Date.now();
    } else if (counter.count % interval === 0) {
      this.logger.debug({ ...context, rateLimited: true, totalOccurrences: counter.count }, message);
      counter.lastLog = Date.now();
    }

    this.counters.set(key, counter);
  }
}

const sampledLogger = new SampledLogger(logger, 0.01);
sampledLogger.debug('Processing order', { orderId: '123' });
sampledLogger.rateLimitedDebug('db:query', 'Slow query detected', {}, { maxBurst: 5, interval: 50 });

Expected output:

Debug logs are sampled at 1% rate. Rate-limited logs: first 5 occurrences are logged, then every 50th occurrence thereafter.

Common Mistakes

  • Logging expected business events as ERROR — a declined credit card is not an ERROR, it is a WARN or INFO.
  • Using DEBUG in production — debug logs are too verbose for production and significantly increase costs.
  • Not having a dynamic log level mechanism — changing log levels requires redeployment or restart.
  • Setting the same log level for all modules — the database module may need DEBUG while the API module stays at INFO.
  • Creating alert fatigue with too many ERROR-level alerts — only ERROR logs that require immediate action should trigger alerts.

Practice Questions

  1. What is the difference between WARN and ERROR?
  2. When should you use DEBUG vs TRACE?
  3. How do you dynamically change log levels in production?
  4. What is log sampling and when should you use it?
  5. How do you reduce log noise without losing diagnostic value?

Challenge

Design a log level Strategy for a payment processing system. Define which events go to each level. Implement dynamic log levels with an admin endpoint. Add rate-limited debugging for high-volume events. Create alerting rules for each level.

FAQ

What log levels should I use?

ERROR: failures requiring immediate action. WARN: unexpected but handled issues. INFO: significant lifecycle events. DEBUG: detailed diagnostics (not in production by default). TRACE: step-by-step execution flow.

How many log levels do I need?

Use the standard five: error, warn, info, debug, trace. Adding more levels creates confusion. These cover everything from critical failures to detailed tracing.

Should ERROR always trigger an alert?

No. ERROR means something went wrong, but not every error needs an alert. Distinguish between ERROR that requires action and expected error conditions. Alert fatigue is dangerous.

How do I change log levels without redeploying?

Expose a secure admin endpoint POST /api/admin/log-level. Store the level in memory (or Redis for multi-instance). Auto-reset to default after a configurable duration.

What is the cost of logging at different levels?

INFO and above: 10-100 entries per second. DEBUG: 100-1000 entries per second. TRACE: 1000+ entries per second. Log volume directly affects storage costs and query performance.

Mini Project

Build a dynamic log level system. Implement: (1) five log levels with hierarchical filtering, (2) admin endpoint for changing levels with auto-reset, (3) per-module level overrides, (4) log sampling for debug/trace, (5) rate-limited logging for high-frequency events.

What's Next

Continue to Error Logging for effective error logging patterns.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro