11.4. Logging & Errors#

11.4.1. Common Pitfalls#

1. Mixing stdout and stderr unintentionally

# Bad: Loses stderr
output=$(command)  # stderr goes to console
echo "$output"     # Only stdout captured

# Good: Capture both when needed
output=$(command 2>&1)  # Both in $output
# or separate them
output=$(command 2>/tmp/err.log)
errors=$(cat /tmp/err.log)

2. Log files filling up disk

# Bad: No rotation, grows indefinitely
echo "Log entry" >> /var/log/app.log

# Good: Implement rotation
# Add to crontab:
# @daily /usr/local/bin/rotate_logs /var/log/app.log

# Or use logrotate configuration in /etc/logrotate.d/

3. Not preserving error context

# Bad: Error context lost
command || echo "Error"

# Better: Save context
if ! output=$(command 2>&1); then
  echo "Command failed with code $?" >&2
  echo "Output was: $output" >&2
  exit 1
fi

4. Forgetting logs need privileges

# Bad: Script can't write to /var/log/ if not root
echo "Error" >> /var/log/app.log

# Better: Use appropriate location
LOG_DIR="${LOG_DIR:-.}"  # Current dir as fallback
mkdir -p "$LOG_DIR"
echo "Error" >> "$LOG_DIR/app.log"

11.4.2. Real-World Example: Comprehensive Logging System#

#!/bin/bash
set -euo pipefail

# APPLICATION LOGGING SYSTEM
readonly APP_NAME="backup_system"
readonly LOG_DIR="/var/log/$APP_NAME"
readonly LOG_FILE="$LOG_DIR/$APP_NAME.log"
readonly ERROR_LOG="$LOG_DIR/error.log"

# Initialize logging
initialize_logging() {
  mkdir -p "$LOG_DIR" || {
    echo "Failed to create log directory" >&2
    return 1
  }
}

# Log function with level
log() {
  local level=$1
  shift
  local message="$@"
  local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
  
  echo "[$timestamp] [$level] $message" >> "$LOG_FILE"
  
  # Also log errors to separate file
  if [[ "$level" == "ERROR" ]]; then
    echo "[$timestamp] $message" >> "$ERROR_LOG"
  fi
}

# Error handler
handle_error() {
  local line=$1
  local exit_code=$2
  
  log "ERROR" "Command failed at line $line (exit code: $exit_code)"
  log "ERROR" "Failed command: $BASH_COMMAND"
  
  # Alert admin for critical errors
  if [[ $exit_code -gt 10 ]]; then
    {
      echo "Critical Error in $APP_NAME"
      echo "Line: $line"
      echo "Exit Code: $exit_code"
      echo "Command: $BASH_COMMAND"
      echo ""
      echo "Recent logs:"
      tail -10 "$ERROR_LOG"
    } | mail -s "CRITICAL: $APP_NAME failed" admin@example.com
  fi
}

trap 'handle_error $LINENO $?' ERR

# Initialize and run
initialize_logging || exit 1
log "INFO" "$APP_NAME starting"

# Do work
log "INFO" "Backup process started"
perform_backup || log "ERROR" "Backup failed"
log "INFO" "$APP_NAME completed"

# Rotate logs if needed
[[ -f "$LOG_FILE" ]] && [[ $(stat -c%s "$LOG_FILE" 2>/dev/null || echo 0) -gt 1048576 ]] && {
  gzip "$LOG_FILE"
  > "$LOG_FILE"
}

11.4.3. Error Redirection Patterns#

11.4.3.1. Separating stdout and stderr#

#!/bin/bash

# Different destinations
command > stdout.log 2> stderr.log

# stdout to file, stderr to console
command > output.log

# stderr to file, stdout to console
command 2> error.log

# Both to file
command > all.log 2>&1

# Stderr to specific location
command 2>/var/log/errors.log

# Append instead of overwrite
command >> output.log 2>&1

11.4.3.2. Conditional Error Handling#

# Capture error output
output=$(command 2>&1) || {
  exit_code=$?
  echo "Command failed with code $exit_code" >&2
  echo "Output: $output" >&2
  exit $exit_code
}

# Discard expected errors
grep pattern file.txt 2>/dev/null || {
  # Pattern not found (expected when using grep as a condition)
  echo "Pattern not found, continuing..."
}

11.4.3.3. Tee for Dual Output#

# Log to file AND display on console
command 2>&1 | tee -a /var/log/app.log

# Append to log while showing errors
command 2>&1 | tee -a /var/log/app.log >&2

# Color output to console, plain to log
command 2>&1 | tee >(cat >> /var/log/plain.log)

11.4.4. Log Levels and Filtering#

11.4.4.1. Standard Log Levels#

# Implement log levels with filtering
declare -r LOG_LEVEL_DEBUG=0
declare -r LOG_LEVEL_INFO=1
declare -r LOG_LEVEL_WARN=2
declare -r LOG_LEVEL_ERROR=3

declare -r CURRENT_LOG_LEVEL=$LOG_LEVEL_INFO

log() {
  local level=$1
  shift
  local message="$@"
  
  # Only log if level meets threshold
  [[ $level -lt $CURRENT_LOG_LEVEL ]] && return 0
  
  case $level in
    0) level_str="DEBUG";;
    1) level_str="INFO";;
    2) level_str="WARN";;
    3) level_str="ERROR";;
  esac
  
  echo "[$(date '+%H:%M:%S')] $level_str: $message"
}

# Usage
log $LOG_LEVEL_DEBUG "Entering function"  # Not shown (below threshold)
log $LOG_LEVEL_INFO "Processing file"     # Shown
log $LOG_LEVEL_ERROR "Operation failed"   # Shown

11.4.4.2. Log Rotation#

#!/bin/bash

rotate_logs() {
  local log_file=$1
  local max_size=${2:-10485760}  # 10MB default
  
  if [[ -f "$log_file" && $(stat -f%z "$log_file" 2>/dev/null || stat -c%s "$log_file") -gt $max_size ]]; then
    # Compress and archive
    gzip -c "$log_file" > "${log_file}.$(date +%Y%m%d_%H%M%S).gz"
    
    # Truncate original
    > "$log_file"
    
    # Remove old archives (keep 7 days)
    find $(dirname "$log_file") -name "$(basename $log_file).*.gz" -mtime +7 -delete
  fi
}

# Run periodically from cron
# @hourly /usr/local/bin/rotate_logs /var/log/app.log

11.4.5. Structured Logging#

Create consistent, parseable logs for debugging and monitoring:

11.4.5.1. Basic Log Format#

#!/bin/bash

log() {
  local level=$1
  shift
  local message="$@"
  
  # ISO 8601 timestamp
  local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
  
  # Log format: [TIMESTAMP] LEVEL MESSAGE
  echo "[$timestamp] $level: $message" >> /var/log/app.log
}

# Usage
log "INFO" "Application started"
log "WARN" "Low memory detected"
log "ERROR" "Failed to connect to database"

11.4.5.2. Structured JSON Logging#

log_json() {
  local level=$1
  local message=$2
  local context=$3
  
  printf '{"timestamp":"%s","level":"%s","message":"%s","context":"%s"}\n' \
    "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
    "$level" \
    "$message" \
    "$context" >> /var/log/app.json.log
}

# Usage (parseable by tools like jq)
log_json "ERROR" "Database connection failed" "host=db1.example.com"

11.4.5.3. Syslog Integration#

#!/bin/bash

# Send to system syslog
logger -t myscript "Application started"
logger -t myscript -p user.error "Critical error occurred"
logger -t myscript -p user.info "Backup completed successfully"

# View logs
tail -f /var/log/syslog | grep myscript