We'll start with the app.rb script, which is the entrypoint of our application. First, require the logger library and create an instance of logger in the constructor of the App class. Just like Rails, our logger uses a log/development.log file.

The logger won't create a log directory for you, so make sure to create it before running the program.

When the application receives a request, i.e. when the call method is invoked, we'll log a message containing the HTTP request method and the request path.

require 'logger'
require_relative 'config/routes'

class App
  attr_reader :logger

  def initialize
    @logger = Logger.new('log/development.log')
  end

  def call(env)
    logger.info "#{env['REQUEST_METHOD']}: #{env['REQUEST_PATH']}"

    headers = { 'Content-Type' => 'text/html' }

    response_html = router.build_response(env)

    [200, headers, [response_html]]
  end

  private
    def router
      Router.instance
    end
end

Alright... let's try this out!

Launch the application by running either rackup or puma command, and visit the application in the browser. First, visit the home page at / and then visit the /articles/index path.

At this point, you'll find a new development.log file has been created in the log directory, containing the following logs.

# Logfile created on 2023-07-29 16:19:13 -0700 by logger.rb/v1.5.3

I, [2023-07-29T17:54:54.618107 #40911]  INFO -- : GET: /
I, [2023-07-29T17:55:01.643705 #40911]  INFO -- : GET: /articles/index

Our logging system is working as expected. Any time someone visits our application, the logger will make an entry in the log file. You can add as much relevant information as you want in the log.

So that was an info message. What happens when something goes wrong?

To log an error, use the error method on the logger object. Here's the modified call method.

def call(env)
  # existing code ...
rescue => e
  logger.error("Oops, something went wrong. Error: #{e.message}")

  # we still have to return a rack-compliant response
  [200, headers, ["Error: #{e.message}"]]
end

I've added a rescue clause that will catch all errors and print the error message to the log file. Finally, we'll return a rack-compliant message containing the error.

To test this, open the ArticlesController class and raise an error from the index action.

require_relative "application_controller"

class ArticlesController < ApplicationController
  def index
    raise "Stack Overflow!"

    @title = "Write Software, Well"
    @tagline = "Learn to program Ruby and build webapps with Rails"
  end
end

Now visit the articles/index path, and you'll see the following error message has been appended to the log file.

I, [2023-07-29T18:05:19.979988 #41201]  INFO -- : GET: /articles/index
E, [2023-07-29T18:05:19.980172 #41201] ERROR -- : Oops, something went wrong. Error: Stack Overflow!

Instead of using string interpolation to insert the error message, you can also use the add method on the logger, which lets you pass a string, an error object, or any object.

def call(env)
  # existing code ...
rescue => e
  logger.add(Logger::ERROR, e)

  # we still have to return a rack-compliant response
  [200, headers, ["Error: #{e.message}"]]
end

This logs the following error message (note the stack trace):

I, [2023-07-30T13:24:38.959108 #48772]  INFO -- : GET: /articles/index
E, [2023-07-30T13:24:38.959283 #48772] ERROR -- : StackOverflow! (RuntimeError)
weby/controllers/articles_controller.rb:5:in `index'
weby/router.rb:30:in `block in get'
weby/router.rb:40:in `build_response'
weby/app.rb:17:in `call'

Congratulations, we've successfully implemented a working logging solution for our application.