-
Notifications
You must be signed in to change notification settings - Fork 21.8k
Commit
…other production concerns
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
require 'digest/md5' | ||
|
||
module ActionDispatch | ||
# Makes a unique request id available to the action_dispatch.request_id env variable (which is then accessible through | ||
# ActionDispatch::Request#uuid) and sends the same id to the client via the X-Request-Id header. | ||
# | ||
# The unique request id is either based off the X-Request-Id header in the request, which would typically be generated | ||
# by a firewall, load balancer, or the web server, or, if this header is not available, a random uuid. If the | ||
# header is accepted from the outside world, we sanitize it to a max of 255 chars and alphanumeric and dashes only. | ||
# | ||
# The unique request id can be used to trace a request end-to-end and would typically end up being part of log files | ||
# from multiple pieces of the stack. | ||
class RequestId | ||
def initialize(app) | ||
@app = app | ||
end | ||
|
||
def call(env) | ||
env["action_dispatch.request_id"] = external_request_id(env) || internal_request_id | ||
|
||
status, headers, body = @app.call(env) | ||
|
||
headers["X-Request-Id"] = env["action_dispatch.request_id"] | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
josevalim
Contributor
|
||
[ status, headers, body ] | ||
end | ||
|
||
private | ||
def external_request_id(env) | ||
if env["HTTP_X_REQUEST_ID"].present? | ||
env["HTTP_X_REQUEST_ID"].gsub(/[^\w\d\-]/, "").first(255) | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
josevalim
Contributor
|
||
end | ||
end | ||
|
||
def internal_request_id | ||
SecureRandom.uuid | ||
This comment has been minimized.
Sorry, something went wrong.
phurni
|
||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
require 'abstract_unit' | ||
|
||
class RequestIdTest < ActiveSupport::TestCase | ||
test "passing on the request id from the outside" do | ||
assert_equal "external-uu-rid", stub_request('HTTP_X_REQUEST_ID' => 'external-uu-rid').uuid | ||
end | ||
|
||
test "ensure that only alphanumeric uurids are accepted" do | ||
assert_equal "X-Hacked-HeaderStuff", stub_request('HTTP_X_REQUEST_ID' => '; X-Hacked-Header: Stuff').uuid | ||
end | ||
|
||
test "ensure that 255 char limit on the request id is being enforced" do | ||
assert_equal "X" * 255, stub_request('HTTP_X_REQUEST_ID' => 'X' * 500).uuid | ||
end | ||
|
||
test "generating a request id when none is supplied" do | ||
assert_match /\w+-\w+-\w+-\w+-\w+/, stub_request.uuid | ||
end | ||
|
||
private | ||
def stub_request(env = {}) | ||
ActionDispatch::RequestId.new(->(env) { [ 200, env, [] ] }).call(env) | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong. |
||
ActionDispatch::Request.new(env) | ||
end | ||
end | ||
|
||
# FIXME: Testing end-to-end doesn't seem to work | ||
# | ||
# class RequestIdResponseTest < ActionDispatch::IntegrationTest | ||
# class TestController < ActionController::Base | ||
# def index | ||
# head :ok | ||
# end | ||
# end | ||
# | ||
# test "request id is passed all the way to the response" do | ||
# with_test_route_set do | ||
# get '/' | ||
# puts @response.headers.inspect | ||
# assert_equal "internal-uu-rid", @response.headers["X-Request-Id"] | ||
# end | ||
# end | ||
# | ||
# | ||
# private | ||
# def with_test_route_set | ||
# with_routing do |set| | ||
# set.draw do | ||
# match ':action', to: ::RequestIdResponseTest::TestController | ||
# end | ||
# | ||
# @app = self.class.build_app(set) do |middleware| | ||
# middleware.use ActionDispatch::RequestId | ||
# end | ||
# | ||
# yield | ||
# end | ||
# end | ||
# end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
module ActiveSupport | ||
# Wraps any standard Logger class to provide tagging capabilities. Examples: | ||
# | ||
# Logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT)) | ||
# Logger.tagged("BCX") { Logger.info "Stuff" } # Logs "[BCX] Stuff" | ||
# Logger.tagged("BCX", "Jason") { Logger.info "Stuff" } # Logs "[BCX] [Jason] Stuff" | ||
# Logger.tagged("BCX") { Logger.tagged("Jason") { Logger.info "Stuff" } } # Logs "[BCX] [Jason] Stuff" | ||
# | ||
# This is used by the default Rails.logger as configured by Railties to make it easy to stamp log lines | ||
# with subdomains, request ids, and anything else to aid debugging of multi-user production applications. | ||
class TaggedLogging | ||
def initialize(logger) | ||
@logger = logger | ||
@tags = [] | ||
This comment has been minimized.
Sorry, something went wrong.
josevalim
Contributor
|
||
end | ||
|
||
def tagged(*tags) | ||
new_tags = Array.wrap(tags).flatten | ||
@tags += new_tags | ||
yield | ||
ensure | ||
new_tags.size.times { @tags.pop } | ||
end | ||
|
||
|
||
def add(severity, message = nil, progname = nil, &block) | ||
@logger.add(severity, "#{tags}#{message}", progname, &block) | ||
end | ||
|
||
|
||
def fatal(progname = nil, &block) | ||
add(@logger.class::FATAL, progname, &block) | ||
end | ||
|
||
def error(progname = nil, &block) | ||
add(@logger.class::ERROR, progname, &block) | ||
end | ||
|
||
def warn(progname = nil, &block) | ||
add(@logger.class::WARN, progname, &block) | ||
end | ||
|
||
def info(progname = nil, &block) | ||
add(@logger.class::INFO, progname, &block) | ||
end | ||
|
||
def debug(progname = nil, &block) | ||
add(@logger.class::DEBUG, progname, &block) | ||
end | ||
|
||
def unknown(progname = nil, &block) | ||
add(@logger.class::UNKNOWN, progname, &block) | ||
end | ||
|
||
|
||
def method_missing(method, *args) | ||
@logger.send(method, *args) | ||
end | ||
|
||
|
||
private | ||
def tags | ||
if @tags.any? | ||
@tags.collect { |tag| "[#{tag}]" }.join(" ") + " " | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
require 'abstract_unit' | ||
require 'active_support/core_ext/logger' | ||
require 'active_support/tagged_logging' | ||
|
||
class TaggedLoggingTest < ActiveSupport::TestCase | ||
setup do | ||
@output = StringIO.new | ||
@logger = ActiveSupport::TaggedLogging.new(Logger.new(@output)) | ||
end | ||
|
||
test "tagged once" do | ||
@logger.tagged("BCX") { @logger.info "Funky time" } | ||
assert_equal "[BCX] Funky time\n", @output.string | ||
end | ||
|
||
test "tagged twice" do | ||
@logger.tagged("BCX") { @logger.tagged("Jason") { @logger.info "Funky time" } } | ||
assert_equal "[BCX] [Jason] Funky time\n", @output.string | ||
end | ||
|
||
test "tagged thrice at once" do | ||
@logger.tagged("BCX", "Jason", "New") { @logger.info "Funky time" } | ||
assert_equal "[BCX] [Jason] [New] Funky time\n", @output.string | ||
end | ||
|
||
test "mixed levels of tagging" do | ||
@logger.tagged("BCX") do | ||
@logger.tagged("Jason") { @logger.info "Funky time" } | ||
@logger.info "Junky time!" | ||
end | ||
|
||
assert_equal "[BCX] [Jason] Funky time\n[BCX] Junky time!\n", @output.string | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
module Rails | ||
module Rack | ||
# Enables easy tagging of any logging activity that occurs within the Rails request cycle. The tags are configured via the | ||
# config.log_tags setting. The tags can either be strings, procs taking a request argument, or the symbols :uuid or :subdomain. | ||
# The latter two are then automatically expanded to request.uuid and request.subdaomins.first -- the two most common tags | ||
# desired in production logs. | ||
class TaggedLogging | ||
def initialize(app, tags = nil) | ||
@app, @tags = app, tags | ||
end | ||
|
||
def call(env) | ||
if @tags | ||
Rails.logger.tagged(compute_tags(env)) { @app.call(env) } | ||
else | ||
@app.call(env) | ||
end | ||
end | ||
|
||
private | ||
def compute_tags(env) | ||
request = ActionDispatch::Request.new(env) | ||
|
||
@tags.collect do |tag| | ||
case tag | ||
when Proc | ||
tag.call(request) | ||
when :uuid | ||
This comment has been minimized.
Sorry, something went wrong.
josevalim
Contributor
|
||
request.uuid | ||
when :subdomain | ||
request.subdomains.first | ||
else | ||
tag | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
5 comments
on commit afde6fd
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be useful to default to including this header in XHR calls initiated from the page too, or should that be counted as a new request stack? E.g. an addition to jquery_ujs.js
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is to identify all logging for the same request. It does not intend to identity a user across requests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is quite useful...off to see if an F5 can generate the uuid.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Build red! Need to update Railties' test/application/rack/logger_test.rb
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like a good candidate for reverting. ❤️
My organization has been doing exactly what your patch here does for a few years now (and amusingly enough, I was literally just updating our Rails-related documentation on it when I stumbled across this commit). However, our header is "X-Request-ID", and that case-sensitive name has ended up in a large number of applications (using a large number of different languages and frameworks, not just Rails), so it would not be trivial for me to change the case of my header name, and as currently written, this particular line of code won't work for me.
Any chance you could change this to X-Request-ID? If not, is there a way to make this more generic? Perhaps allow the application developer to specify the name of the header instead of hard-coding it here?