Skip to Content
Course content

68: Logger for Application Logging

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When you're first building out a feature—say, a system that processes credit card payments for an online store—your first instinct for tracking what's happening is probably puts. It's fast, it's immediate, and it shows up right there in your terminal. I've seen countless developers leave a trail of puts "Payment processed for #{order_id}" throughout their codebase, thinking they've implemented "logging."

def process_payment(order)
  puts "Attempting to charge #{order.amount} for Order ##{order.id}"
  result = PaymentGateway.charge(order.amount)
  
  if result.success?
    puts "Payment successful!"
  else
    puts "Payment failed: #{result.error_message}"
  end
end

The visibility gap in production

The problem with puts is that it's a blunt instrument. In your local development environment, it's fine. But once that code hits a production server, puts becomes a liability. If your app is running as a background service or inside a container, where does that output actually go? Often, it ends up in a generic system log file that's mixed with thousands of other lines of noise, or worse, it's swallowed entirely.

Beyond the destination, you have no way to filter the noise. Imagine your app is processing 10,000 orders an hour. You don't want to see "Payment successful!" 10,000 times in your logs—that's just clutter. But you absolutely need to know the second a payment fails. With puts, it's all or nothing. You can't tell the program, "Only tell me about the errors today, but keep the detailed debug info available if I need to turn it on later."

Bringing in the Logger class

This is why Ruby provides the Logger library. It's a standard part of the language, and it solves the destination and filtering problems in one go. Instead of printing to the screen, you create a logger instance that can write to a file, a string, or STDOUT, and you categorize every message by its severity.

require 'logger'

# I usually set this up as a global or a constant in a configuration file
$log = Logger.new('log/production.log')

# Now we can be specific about the importance of the message
def process_payment(order)
  $log.info("Attempting to charge #{order.amount} for Order ##{order.id}")
  result = PaymentGateway.charge(order.amount)
  
  if result.success?
    $log.info("Payment successful for Order ##{order.id}")
  else
    $log.error("Payment failed for Order ##{order.id}: #{result.error_message}")
  end
rescue StandardError => e
  $log.fatal("System crash during payment processing: #{e.message}")
end

Tuning the volume of your logs

The real magic happens when you realize you can change the "level" of the logger without changing a single line of your logic. The Logger class recognizes a hierarchy: DEBUG, INFO, WARN, ERROR, and FATAL.

If you're debugging a weird edge case in staging, you can set $log.level = Logger::DEBUG. Suddenly, every tiny detail you've logged is visible. But in production, you'll likely set $log.level = Logger::INFO or Logger::WARN. This tells Ruby to ignore everything below that threshold. All those DEBUG messages you left in the code to help you build the feature? They're still there, but they won't take up a single byte of disk space or slow down your app in production.

I'll give you a tip from the trenches: always include a unique identifier (like an order ID or user ID) in your logs. A log that says "Payment failed" is useless. A log that says "Payment failed for Order #5501" is a map that leads you straight to the bug.




📋 Practical Task

Implementing a Payment Gateway Audit Log

You are tasked with upgrading a legacy payment processing script that currently uses puts. You need to replace the print statements with a proper Logger implementation to make the system production-ready.

Requirements:

  • Require the logger library.
  • Create a logger instance that writes to a file named payment_audit.log.
  • Replace the puts statement for the "starting process" message with info.
  • Replace the puts statement for the "payment failed" message with error.
  • Add a rescue block to catch any StandardError and log it as fatal.
  • Set the logger level to Logger::INFO so that debug messages (if any were added) would be ignored.
# Legacy code to be fixed:
def process_transaction(id, amount)
  puts "Processing transaction #{id} for $#{amount}..."
  
  # Simulate a random failure
  if rand > 0.8
    raise "Connection Timeout"
  elsif rand > 0.5
    puts "Transaction #{id} failed: Insufficient Funds"
    return false
  end

  puts "Transaction #{id} completed successfully."
  true
end

process_transaction("TXN123", 50.00)
Rating
0 0

There are no comments for now.

to be the first to leave a comment.