diff --git a/lib/rorvswild/plugin/middleware.rb b/lib/rorvswild/plugin/middleware.rb index ea514c3..5c4fe24 100644 --- a/lib/rorvswild/plugin/middleware.rb +++ b/lib/rorvswild/plugin/middleware.rb @@ -3,6 +3,52 @@ module RorVsWild module Plugin class Middleware + module RequestQueueTime + REQUEST_START_HEADER = 'HTTP_X_REQUEST_START'.freeze + QUEUE_START_HEADER = 'HTTP_X_QUEUE_START'.freeze + MIDDLEWARE_START_HEADER = 'HTTP_X_MIDDLEWARE_START'.freeze + + ACCEPTABLE_HEADERS = [ + REQUEST_START_HEADER, + QUEUE_START_HEADER, + MIDDLEWARE_START_HEADER + ].freeze + + MINIMUM_TIMESTAMP = 1577836800.freeze # 2020/01/01 UTC + DIVISORS = [1_000_000, 1_000, 1].freeze + + def parse_queue_time_header(headers) + return unless headers + + earliest = nil + + ACCEPTABLE_HEADERS.each do |header| + next unless headers[header] + + timestamp = parse_timestamp(headers[header].gsub("t=", "")) + if timestamp && (!earliest || timestamp < earliest) + earliest = timestamp + end + end + + [earliest, Time.now.to_f].min if earliest + end + + private + + def parse_timestamp(timestamp) + DIVISORS.each do |divisor| + begin + t = (timestamp.to_f / divisor) + return t if t > MINIMUM_TIMESTAMP + rescue RangeError + end + end + end + end + + include RequestQueueTime + def self.setup return if @installed Rails.application.config.middleware.unshift(RorVsWild::Plugin::Middleware, nil) if defined?(Rails) @@ -16,6 +62,7 @@ def initialize(app, config) def call(env) RorVsWild.agent.start_request RorVsWild.agent.current_data[:path] = env["ORIGINAL_FULLPATH"] + RorVsWild.agent.current_data[:queue_time] = calculate_queue_time(env) section = RorVsWild::Section.start section.file, section.line = rails_engine_location section.commands << "Rails::Engine#call" @@ -28,6 +75,12 @@ def call(env) private + def calculate_queue_time(headers) + queue_time_from_header = parse_queue_time_header(headers) + + ((Time.now.to_f - queue_time_from_header) * 1000).round if queue_time_from_header + end + def rails_engine_location @rails_engine_location = ::Rails::Engine.instance_method(:call).source_location end diff --git a/test/plugin/middleware_test.rb b/test/plugin/middleware_test.rb index 94e1a8a..3ffa3bc 100644 --- a/test/plugin/middleware_test.rb +++ b/test/plugin/middleware_test.rb @@ -5,7 +5,7 @@ class RorVsWild::Plugin::MiddlewareTest < Minitest::Test def test_callback agent # Load agent - request = {"ORIGINAL_FULLPATH" => "/foo/bar"} + request = { "ORIGINAL_FULLPATH" => "/foo/bar" } app = mock(call: nil) middleware = RorVsWild::Plugin::Middleware.new(app, nil) middleware.stubs(rails_engine_location: ["/rails/lib/engine.rb", 12]) @@ -13,6 +13,45 @@ def test_callback assert_equal("/foo/bar", agent.current_data[:path]) assert_equal(1, agent.current_data[:sections].size) assert_equal("Rails::Engine#call", agent.current_data[:sections][0].command) + assert_nil(agent.current_data[:queue_time]) end -end + def test_queue_time_secs + agent # Load agent + request_start = unix_timestamp_seconds - 0.123 + request = {"HTTP_X_REQUEST_START" => request_start.to_s} + app = mock(call: nil) + middleware = RorVsWild::Plugin::Middleware.new(app, nil) + middleware.stubs(rails_engine_location: ["/rails/lib/engine.rb", 12]) + middleware.call(request) + assert_in_delta(123, agent.current_data[:queue_time], 10) + end + + def test_queue_time_millis + agent # Load agent + request_start = unix_timestamp_seconds * 1000 - 234 + request = { "HTTP_X_QUEUE_START" => request_start.to_s } + app = mock(call: nil) + middleware = RorVsWild::Plugin::Middleware.new(app, nil) + middleware.stubs(rails_engine_location: ["/rails/lib/engine.rb", 12]) + middleware.call(request) + assert_in_delta(234, agent.current_data[:queue_time], 10) + end + + def test_queue_time_micros + agent # Load agent + request_start = unix_timestamp_seconds * 1_000_000 - 345_000 + request = { "HTTP_X_MIDDLEWARE_START" => request_start.to_s } + app = mock(call: nil) + middleware = RorVsWild::Plugin::Middleware.new(app, nil) + middleware.stubs(rails_engine_location: ["/rails/lib/engine.rb", 12]) + middleware.call(request) + assert_in_delta(345, agent.current_data[:queue_time], 10) + end + + private + + def unix_timestamp_seconds + Time.now.to_f + end +end