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:
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
Rails on Rack: Official Rails Guides explaining the middleware API
What is Rack Middleware?: Lots of good answers on this old question on Stack Overflow
Pipeline Design Pattern: Formal theory behind the architectural concepts behind Rack and the concept of middleware
Rack Middleware (Railscasts): Old is gold. I miss Railscasts.