Skip to content

Commit afde6fd

Browse files
author
David Heinemeier Hansson
committed
Added X-Request-Id tracking and TaggedLogging to easily log that and other production concerns
1 parent 3a746f7 commit afde6fd

File tree

17 files changed

+272
-5
lines changed

17 files changed

+272
-5
lines changed

actionpack/CHANGELOG

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
*Rails 3.2.0 (unreleased)*
22

3+
* Added ActionDispatch::RequestId middleware that'll make a unique X-Request-Id header available to the response and enables the ActionDispatch::Request#uuid method. This makes it easy to trace requests from end-to-end in the stack and to identify individual requests in mixed logs like Syslog [DHH]
4+
35
* Limit the number of options for select_year to 1000.
46

57
Pass the :max_years_allowed option to set your own limit.

actionpack/lib/action_dispatch.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ module ActionDispatch
4747
end
4848

4949
autoload_under 'middleware' do
50+
autoload :RequestId
5051
autoload :BestStandardsSupport
5152
autoload :Callbacks
5253
autoload :Cookies

actionpack/lib/action_dispatch/http/request.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,16 @@ def remote_ip
177177
@remote_ip ||= (@env["action_dispatch.remote_ip"] || ip).to_s
178178
end
179179

180+
# Returns the unique request id, which is based off either the X-Request-Id header that can
181+
# be generated by a firewall, load balancer, or web server or by the RequestId middleware
182+
# (which sets the action_dispatch.request_id environment variable).
183+
#
184+
# This unique ID is useful for tracing a request from end-to-end as part of logging or debugging.
185+
# This relies on the rack variable set by the ActionDispatch::RequestId middleware.
186+
def uuid
187+
@uuid ||= env["action_dispatch.request_id"]
188+
end
189+
180190
# Returns the lowercase name of the HTTP server software.
181191
def server_software
182192
(@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
require 'digest/md5'
2+
3+
module ActionDispatch
4+
# Makes a unique request id available to the action_dispatch.request_id env variable (which is then accessible through
5+
# ActionDispatch::Request#uuid) and sends the same id to the client via the X-Request-Id header.
6+
#
7+
# The unique request id is either based off the X-Request-Id header in the request, which would typically be generated
8+
# by a firewall, load balancer, or the web server, or, if this header is not available, a random uuid. If the
9+
# header is accepted from the outside world, we sanitize it to a max of 255 chars and alphanumeric and dashes only.
10+
#
11+
# The unique request id can be used to trace a request end-to-end and would typically end up being part of log files
12+
# from multiple pieces of the stack.
13+
class RequestId
14+
def initialize(app)
15+
@app = app
16+
end
17+
18+
def call(env)
19+
env["action_dispatch.request_id"] = external_request_id(env) || internal_request_id
20+
21+
status, headers, body = @app.call(env)
22+
23+
headers["X-Request-Id"] = env["action_dispatch.request_id"]
24+
[ status, headers, body ]
25+
end
26+
27+
private
28+
def external_request_id(env)
29+
if env["HTTP_X_REQUEST_ID"].present?
30+
env["HTTP_X_REQUEST_ID"].gsub(/[^\w\d\-]/, "").first(255)
31+
end
32+
end
33+
34+
def internal_request_id
35+
SecureRandom.uuid
36+
end
37+
end
38+
end
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
require 'abstract_unit'
2+
3+
class RequestIdTest < ActiveSupport::TestCase
4+
test "passing on the request id from the outside" do
5+
assert_equal "external-uu-rid", stub_request('HTTP_X_REQUEST_ID' => 'external-uu-rid').uuid
6+
end
7+
8+
test "ensure that only alphanumeric uurids are accepted" do
9+
assert_equal "X-Hacked-HeaderStuff", stub_request('HTTP_X_REQUEST_ID' => '; X-Hacked-Header: Stuff').uuid
10+
end
11+
12+
test "ensure that 255 char limit on the request id is being enforced" do
13+
assert_equal "X" * 255, stub_request('HTTP_X_REQUEST_ID' => 'X' * 500).uuid
14+
end
15+
16+
test "generating a request id when none is supplied" do
17+
assert_match /\w+-\w+-\w+-\w+-\w+/, stub_request.uuid
18+
end
19+
20+
private
21+
def stub_request(env = {})
22+
ActionDispatch::RequestId.new(->(env) { [ 200, env, [] ] }).call(env)
23+
ActionDispatch::Request.new(env)
24+
end
25+
end
26+
27+
# FIXME: Testing end-to-end doesn't seem to work
28+
#
29+
# class RequestIdResponseTest < ActionDispatch::IntegrationTest
30+
# class TestController < ActionController::Base
31+
# def index
32+
# head :ok
33+
# end
34+
# end
35+
#
36+
# test "request id is passed all the way to the response" do
37+
# with_test_route_set do
38+
# get '/'
39+
# puts @response.headers.inspect
40+
# assert_equal "internal-uu-rid", @response.headers["X-Request-Id"]
41+
# end
42+
# end
43+
#
44+
#
45+
# private
46+
# def with_test_route_set
47+
# with_routing do |set|
48+
# set.draw do
49+
# match ':action', to: ::RequestIdResponseTest::TestController
50+
# end
51+
#
52+
# @app = self.class.build_app(set) do |middleware|
53+
# middleware.use ActionDispatch::RequestId
54+
# end
55+
#
56+
# yield
57+
# end
58+
# end
59+
# end

activesupport/CHANGELOG

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
*Rails 3.2.0 (unreleased)*
22

3+
* Added ActiveSupport:TaggedLogging that can wrap any standard Logger class to provide tagging capabilities [DHH]
4+
5+
Logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT))
6+
Logger.tagged("BCX") { Logger.info "Stuff" } # Logs "[BCX] Stuff"
7+
Logger.tagged("BCX", "Jason") { Logger.info "Stuff" } # Logs "[BCX] [Jason] Stuff"
8+
Logger.tagged("BCX") { Logger.tagged("Jason") { Logger.info "Stuff" } } # Logs "[BCX] [Jason] Stuff"
9+
310
* Added safe_constantize that constantizes a string but returns nil instead of an exception if the constant (or part of it) does not exist [Ryan Oblak]
411

512
* ActiveSupport::OrderedHash is now marked as extractable when using Array#extract_options! [Prem Sichanugrist]

activesupport/lib/active_support.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ module ActiveSupport
7171
autoload :OrderedOptions
7272
autoload :Rescuable
7373
autoload :StringInquirer
74+
autoload :TaggedLogging
7475
autoload :XmlMini
7576
end
7677

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
module ActiveSupport
2+
# Wraps any standard Logger class to provide tagging capabilities. Examples:
3+
#
4+
# Logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT))
5+
# Logger.tagged("BCX") { Logger.info "Stuff" } # Logs "[BCX] Stuff"
6+
# Logger.tagged("BCX", "Jason") { Logger.info "Stuff" } # Logs "[BCX] [Jason] Stuff"
7+
# Logger.tagged("BCX") { Logger.tagged("Jason") { Logger.info "Stuff" } } # Logs "[BCX] [Jason] Stuff"
8+
#
9+
# This is used by the default Rails.logger as configured by Railties to make it easy to stamp log lines
10+
# with subdomains, request ids, and anything else to aid debugging of multi-user production applications.
11+
class TaggedLogging
12+
def initialize(logger)
13+
@logger = logger
14+
@tags = []
15+
end
16+
17+
def tagged(*tags)
18+
new_tags = Array.wrap(tags).flatten
19+
@tags += new_tags
20+
yield
21+
ensure
22+
new_tags.size.times { @tags.pop }
23+
end
24+
25+
26+
def add(severity, message = nil, progname = nil, &block)
27+
@logger.add(severity, "#{tags}#{message}", progname, &block)
28+
end
29+
30+
31+
def fatal(progname = nil, &block)
32+
add(@logger.class::FATAL, progname, &block)
33+
end
34+
35+
def error(progname = nil, &block)
36+
add(@logger.class::ERROR, progname, &block)
37+
end
38+
39+
def warn(progname = nil, &block)
40+
add(@logger.class::WARN, progname, &block)
41+
end
42+
43+
def info(progname = nil, &block)
44+
add(@logger.class::INFO, progname, &block)
45+
end
46+
47+
def debug(progname = nil, &block)
48+
add(@logger.class::DEBUG, progname, &block)
49+
end
50+
51+
def unknown(progname = nil, &block)
52+
add(@logger.class::UNKNOWN, progname, &block)
53+
end
54+
55+
56+
def method_missing(method, *args)
57+
@logger.send(method, *args)
58+
end
59+
60+
61+
private
62+
def tags
63+
if @tags.any?
64+
@tags.collect { |tag| "[#{tag}]" }.join(" ") + " "
65+
end
66+
end
67+
end
68+
end
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
require 'abstract_unit'
2+
require 'active_support/core_ext/logger'
3+
require 'active_support/tagged_logging'
4+
5+
class TaggedLoggingTest < ActiveSupport::TestCase
6+
setup do
7+
@output = StringIO.new
8+
@logger = ActiveSupport::TaggedLogging.new(Logger.new(@output))
9+
end
10+
11+
test "tagged once" do
12+
@logger.tagged("BCX") { @logger.info "Funky time" }
13+
assert_equal "[BCX] Funky time\n", @output.string
14+
end
15+
16+
test "tagged twice" do
17+
@logger.tagged("BCX") { @logger.tagged("Jason") { @logger.info "Funky time" } }
18+
assert_equal "[BCX] [Jason] Funky time\n", @output.string
19+
end
20+
21+
test "tagged thrice at once" do
22+
@logger.tagged("BCX", "Jason", "New") { @logger.info "Funky time" }
23+
assert_equal "[BCX] [Jason] [New] Funky time\n", @output.string
24+
end
25+
26+
test "mixed levels of tagging" do
27+
@logger.tagged("BCX") do
28+
@logger.tagged("Jason") { @logger.info "Funky time" }
29+
@logger.info "Junky time!"
30+
end
31+
32+
assert_equal "[BCX] [Jason] Funky time\n[BCX] Junky time!\n", @output.string
33+
end
34+
end

railties/CHANGELOG

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
*Rails 3.2.0 (unreleased)*
22

3+
* Added Rails::Rack::TaggedLogging middleware by default that will apply any tags set in config.log_tags to the newly ActiveSupport::TaggedLogging Rails.logger. This makes it easy to tag log lines with debug information like subdomain and request id -- both very helpful in debugging multi-user production applications [DHH]
4+
35
* Default options to `rails new` can be set in ~/.railsrc [Guillermo Iguaran]
46

57
* Added destroy alias to Rails engines. [Guillermo Iguaran]

0 commit comments

Comments
 (0)