Как найти текущий абстрактный маршрут в Rails middware

Версия Rails: '~ > 4.2.7.1'

Версия Spree: "3.1.1"

TL;DR: Как получить маршрут как /api/products/:id или контроллер и действие этого маршрута в промежуточном программном обеспечении приложения Rails 4.

Детали:

Я добавляю промежуточное программное обеспечение в моем приложении rails, которое похоже на gem scout_statsd_rack. Это добавляет следующее middleware в приложение 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, поскольку мой puropose должен отслеживать производительность API /api/products/:id.

env['REQUEST_PATH'] и env['REQUEST_URI'] также дает тот же ответ.

Я попробовал answer здесь и здесь:

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

или как это

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

Но он дал следующую ошибку:

NoMethodError (undefined метод 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/travel/router.rb: 59: in recognize'
vendor/bundle/gems/scout_statsd_rack-0.1.7/lib/scout_statsd_rack.rb:27:in
call '

Этот answer обсуждает request.original_url, но как мне получить доступ к переменной request, я думаю, что она должна быть такой же, как env, но не в состоянии получить маршрут как хотите от этого.

Изменить # 1

Вы можете увидеть образец repo здесь, с кодом промежуточного программного обеспечения 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=#<ActionDispatch::Routing::Mapper::Constraints:0x007fa1833ace70 @dispatcher=false, @app=Spree::Core::Engine, @constraints=[]>, @path=#<ActionDispatch::Journey::Path::Pattern:0x007fa1833acc90 @spec=#<ActionDispatch::Journey::Nodes::Slash:0x007fa1833ad230 @left="/", @memo=nil>, @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::Journey::Format:0x007fa1833ac588 @parts=["/"], @children=[], @parameters=[]>>

Я также нажал на изменения здесь.

Ответ 1

Вам нужно передать метод ActionDispatch::Request или Rack::Request в recognize. Вот пример из другого приложения:

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, но поскольку у вас установлен только двигатель spree, вам нужно использовать другой класс

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

Я думаю, что лучше всего найти общее решение, которое работает с любой комбинацией обычных маршрутов и движков, но это будет работать в вашем случае.

Обновление # 2

Для более общего решения вам нужно посмотреть на источник Rails-маршрутизатора, который вы можете найти в модуле ActionDispatch. Посмотрите на Routing и Journey модули. Я узнал, что возвращаемый маршрут из метода 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, если вам не нужна рекурсивная проверка.