In the previous lesson, we implemented the logging functionality for our simple web application written in vanilla Ruby.

However, one thing that's been bothering me is how the logging-related code is scattered throughout our application, i.e. app.rb file.

# app.rb

require 'debug'
require 'logger'
require 'zeitwerk'

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]]
  rescue => e
    logger.error("Oops, something went wrong. Error: #{e.message}")
    [200, headers, ["Error: #{e.message}"]]
  end

  private
    # ...
end

If you remember, whenever the Puma web server receives a new HTTP request, it calls the call method on the App class. Since we want to log each and every incoming request, logging is an excellent candidate for extraction as a middleware. That way, it can live in its own module without affecting the rest of the app.

In fact, we're actually already using some middleware in our simple application. The reloader middleware checks if the application code is changed or not and reloads the code if it did. The config.ru file holds the middleware chain.

# config.ru

require 'rack'
require_relative 'app'

# Reload source after change
use Rack::Reloader, 0

# Serve all requests beginning with /public 
# from the "public" folder and favicon.ico
# from the current directory
use Rack::Static, urls: ['/public', "/favicon.ico"]

run App.new

The basic idea is to insert the logging middleware into this chain. The logging middleware will log a request, then call the call method of the next handler in the chain, and return the response.

Let's create a new middleware directory in our project and add a logging.rb file in it, which will contain the logging-related code.

# middleware/logging.rb

require 'logger'

class Logging
  attr_reader :logger

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

  def call(env)
    logger.info "#{env['REQUEST_METHOD']}: #{env['REQUEST_PATH']}"
    @app.call(env)  # call the next middleware or our application
  rescue => e
    logger.error("Oops, something went wrong. Error: #{e.message}")
    [200, { 'Content-Type' => 'text/html' }, ["Error: #{e.message}"]]
  end
end

Next, we need to tell Rack to insert this logging middleware just before our main application.

# config.ru

require 'rack'

require_relative 'app'
require_relative 'middleware/logging'

# other middleware

use Logging

run App.new

Finally, we can get rid of all the logging code from app.rb. Here's the diff:

diff.png

Nice and clean.

Start the web server by running puma in the terminal, and hit the URL localhost:9292. If you open the development.log file, you'll notice that the application is still logging the incoming requests as expected.

Additional Resources