如何在Rails middware中找到当前的抽象路由

Rails版本:’〜> 4.2.7.1′

狂欢版:’3.1.1′

TlDr:如何在Rails 4应用程序的中间件中获取路由as /api/products/:id或控制器以及该路由的操作。

细节:

我在rails应用程序中添加了一个类似于gem scout_statsd_rack的中间件 。 这会将以下中间件添加到rails应用程序以通过statsd发送指标:

 def call(env) (status, headers, body), response_time = call_with_timing(env) statsd.timing("#{env['REQUEST_PATH']}.response", response_time) statsd.increment("#{env['REQUEST_PATH']}.response_codes.#{status.to_s.gsub(/\d{2}$/,'xx')}") # Rack response [status, headers, body] rescue Exception => exception statsd.increment("#{env['REQUEST_PATH']}.response_codes.5xx") raise end def call_with_timing(env) start = Time.now result = @app.call(env) [result, ((Time.now - start) * 1000).round] end 

我想要的是在中间件中找到当前路由,以便我可以发送特定于每个路由的度量。

我试过这里描述的方法,它告诉env['PATH_INFO']可以提供路径,但是它提供了这样的URL参数: /api/products/4但我想要的是/api/products/:id as我的目的是跟踪/api/products/:id API的性能。

env['REQUEST_PATH']env['REQUEST_URI']也给出相同的响应。

我试过这里和这里提供的答案:

 Rails.application.routes.router.recognize({"path_info" => env['PATH_INFO']}) 

或者像这样

 Rails.application.routes.router.recognize(env['PATH_INFO']) 

但它给出了以下错误:

NoMethodError( path_info' for {"path_info"=>"/api/v1/products/4"}:Hash):
vendor/bundle/gems/actionpack-4.2.7.1/lib/action_dispatch/journey/router.rb:100:in
未定义方法path_info' for {"path_info"=>"/api/v1/products/4"}:Hash):
vendor/bundle/gems/actionpack-4.2.7.1/lib/action_dispatch/journey/router.rb:100:in
path_info' for {"path_info"=>"/api/v1/products/4"}:Hash):
vendor/bundle/gems/actionpack-4.2.7.1/lib/action_dispatch/journey/router.rb:100:in
path_info' for {"path_info"=>"/api/v1/products/4"}:Hash):
vendor/bundle/gems/actionpack-4.2.7.1/lib/action_dispatch/journey/router.rb:100:in
find_routes’
vendor / bundle / gems / actionpack-4.2.7.1 / lib / action_dispatch / journey / router.rb:59: recognize'
vendor/bundle/gems/scout_statsd_rack-0.1.7/lib/scout_statsd_rack.rb:27:in
recognize'
vendor/bundle/gems/scout_statsd_rack-0.1.7/lib/scout_statsd_rack.rb:27:in
recognize'
vendor/bundle/gems/scout_statsd_rack-0.1.7/lib/scout_statsd_rack.rb:27:in
call’

这个答案讨论了request.original_url ,但我如何访问变量request ,我认为它应该与env相同但不能从中获取路由。

编辑#1

您可以在此处查看示例仓库,其中包含rails中间件代码,可以按照README中的说明进行设置,并且可以访问此API: http://localhost:3000/api/v1/products/1

编辑#2

我试过@MichałMłoźniak给出的方法如下:

 def call(env) (status, headers, body), response_time = call_with_timing(env) request = ActionDispatch::Request.new(env) request = Rack::Request.new("PATH_INFO" => env['REQUEST_PATH'], "REQUEST_METHOD" => env["REQUEST_METHOD"]) Rails.application.routes.router.recognize(request) { |route, params| puts "I am here" puts params.inspect puts route.inspect } 

但我得到了以下回应:

 I am here {} #<ActionDispatch::Journey::Route:0x007fa1833ac628 @name="spree", @app=#, @path=#<ActionDispatch::Journey::Path::Pattern:0x007fa1833acc90 @spec=#, @requirements={}, @separators="/.?", @anchored=false, @names=[], @optional_names=[], @required_names=[], @re=/\A\//, @offsets=[0]>, @constraints={:required_defaults=>[]}, @defaults={}, @required_defaults=nil, @required_parts=[], @parts=[], @decorated_ast=nil, @precedence=1, @path_formatter=#> 

我也在这里推动了变化。

您需要传递ActionDispatch::RequestRack::Requestrecognize方法。 这是另一个应用程序的示例:

 main:0> req = Rack::Request.new("PATH_INFO" => "/customers/10", "REQUEST_METHOD" => "GET") main:0> Rails.application.routes.router.recognize(req) { |route, params| puts params.inspect }; nil {:controller=>"customers", :action=>"show", :id=>"10"} => nil 

这同样适用于ActionDispatch::Request 。 在中间件中,您可以轻松创建此对象:

 request = ActionDispatch::Request.new(env) 

如果您需要有关已识别路径的更多信息,可以通过recognize方法查看要阻止的路径对象。

更新

上面的解决方案适用于普通的Rails路由,但由于你只安装了狂欢引擎,你需要使用不同的类

 request = ActionDispatch::Request.new(env) Spree::Core::Engine.routes.router.recognize(request) { |route, params| puts params.inspect } 

我想最好的是找到适用于普通路由和引擎的任意组合的通用解决方案,但这将适用于您的情况。

更新#2

有关更一般的解决方案,您需要查看Rails路由器的源代码,您可以在ActionDispatch模块中找到它。 查看RoutingJourney模块。 我发现如果这是一个调度员,可以测试来自recognize方法的返回路由。

 request = ActionDispatch::Request.new(env) Rails.application.routes.router.recognize(req) do |route, params| if route.dispatcher? # if this is a dispatcher, params should have everything you need puts params else # you need to go deeper # route.app.app will be Spree::Core::Engine route.app.app.routes.router.recognize(request) do |route, params| puts params.inspect } end end 

这种方法适用于您的应用,但不一般。 例如,如果你安装了sidekiq, route.app.app将是Sidekiq::Web因此需要以不同的方式处理它。 基本上要有一般解决方案,您需要处理Rails路由器支持的所有可安装引擎。

我想最好能够构建一些能够涵盖当前应用中所有情况的东西。 所以要记住的是,当识别出初始请求时,产生黑色的route值可以是调度员。 如果是,你有正常的Rails路由,如果不是,你需要递归检查。