Skip to content

Commit 0547b16

Browse files
authored
Add Server Timing middleware (rails#36289)
* Add Server Timing middleware What is server timing? It's a specification that defines how the server can communicate the user-agent performance metrics about the request it is responding to. Here's the official specification: https://www.w3.org/TR/server-timing/#dfn-server-timing-header-field How does it work? This introduces a new `ServerTiming` middleware only on `development` by default, this is done using the `config.server_timing` setting. It works by subscribing to all `ActiveSupport::Notifications` and adding their duration to the `Server-Timing` header. Why is this useful? It makes looking at performance metrics in development much easier, especially when using Chrome dev tools which includes the metrics in the Network Inspector. Here's an example: ![](https://d3vv6lp55qjaqc.cloudfront.net/items/371h2y3B3a0U470j040u/Image%202019-05-15%20at%205.40.37%20PM.png?) Paired on this with @guilleiguaran
1 parent bac0038 commit 0547b16

File tree

7 files changed

+112
-1
lines changed

7 files changed

+112
-1
lines changed

actionpack/lib/action_dispatch.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ class MissingController < NameError
6767
autoload :PublicExceptions
6868
autoload :Reloader
6969
autoload :RemoteIp
70+
autoload :ServerTiming
7071
autoload :ShowExceptions
7172
autoload :SSL
7273
autoload :Static
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# frozen_string_literal: true
2+
3+
require "active_support/notifications"
4+
5+
module ActionDispatch
6+
class ServerTiming
7+
SERVER_TIMING_HEADER = "Server-Timing"
8+
9+
def initialize(app)
10+
@app = app
11+
end
12+
13+
def call(env)
14+
events = []
15+
subscriber = ActiveSupport::Notifications.subscribe(/.*/) do |*args|
16+
events << ActiveSupport::Notifications::Event.new(*args)
17+
end
18+
19+
status, headers, body = begin
20+
@app.call(env)
21+
ensure
22+
ActiveSupport::Notifications.unsubscribe(subscriber)
23+
end
24+
25+
header_info = events.group_by(&:name).map do |event_name, events_collection|
26+
"#{event_name};dur=#{events_collection.sum(&:duration)}"
27+
end
28+
headers[SERVER_TIMING_HEADER] = header_info.join(", ")
29+
30+
[ status, headers, body ]
31+
end
32+
end
33+
end
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# frozen_string_literal: true
2+
3+
require "abstract_unit"
4+
5+
class ServerTimingTest < ActionDispatch::IntegrationTest
6+
class TestController < ActionController::Base
7+
def index
8+
head :ok
9+
end
10+
11+
def show
12+
ActiveSupport::Notifications.instrument("custom.event") do
13+
true
14+
end
15+
head :ok
16+
end
17+
18+
def create
19+
ActiveSupport::Notifications.instrument("custom.event") do
20+
raise
21+
end
22+
end
23+
end
24+
25+
test "server timing header is included in the response" do
26+
with_test_route_set do
27+
get "/"
28+
assert_match(/\w+/, @response.headers["Server-Timing"])
29+
end
30+
end
31+
32+
test "includes default action controller events duration" do
33+
with_test_route_set do
34+
get "/"
35+
assert_match(/start_processing.action_controller;dur=\w+/, @response.headers["Server-Timing"])
36+
assert_match(/process_action.action_controller;dur=\w+/, @response.headers["Server-Timing"])
37+
end
38+
end
39+
40+
test "includes custom active support events duration" do
41+
with_test_route_set do
42+
get "/id"
43+
assert_match(/custom.event;dur=\w+/, @response.headers["Server-Timing"])
44+
end
45+
end
46+
47+
test "ensures it doesn't leak subscriptions when the app crashes" do
48+
with_test_route_set do
49+
post "/"
50+
assert_not ActiveSupport::Notifications.notifier.listening?("custom.event")
51+
end
52+
end
53+
54+
private
55+
def with_test_route_set
56+
with_routing do |set|
57+
set.draw do
58+
get "/", to: ::ServerTimingTest::TestController.action(:index)
59+
get "/id", to: ::ServerTimingTest::TestController.action(:show)
60+
post "/", to: ::ServerTimingTest::TestController.action(:create)
61+
end
62+
63+
@app = self.class.build_app(set) do |middleware|
64+
middleware.use ActionDispatch::ServerTiming
65+
end
66+
67+
yield
68+
end
69+
end
70+
end

railties/lib/rails/application/configuration.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class Configuration < ::Rails::Engine::Configuration
2121
:read_encrypted_secrets, :log_level, :content_security_policy_report_only,
2222
:content_security_policy_nonce_generator, :content_security_policy_nonce_directives,
2323
:require_master_key, :credentials, :disable_sandbox, :add_autoload_paths_to_load_path,
24-
:rake_eager_load
24+
:rake_eager_load, :server_timing
2525

2626
attr_reader :encoding, :api_only, :loaded_config_version
2727

@@ -74,6 +74,7 @@ def initialize(*)
7474
@add_autoload_paths_to_load_path = true
7575
@permissions_policy = nil
7676
@rake_eager_load = false
77+
@server_timing = false
7778
end
7879

7980
# Loads default configurations. See {the result of the method for each version}[https://guides.rubyonrails.org/configuring.html#results-of-config-load-defaults].

railties/lib/rails/application/default_middleware_stack.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def build_stack
4242

4343
middleware.use ::ActionDispatch::Executor, app.executor
4444

45+
middleware.use ::ActionDispatch::ServerTiming if config.server_timing
4546
middleware.use ::Rack::Runtime
4647
middleware.use ::Rack::MethodOverride unless config.api_only
4748
middleware.use ::ActionDispatch::RequestId, header: config.action_dispatch.request_id_header

railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ Rails.application.configure do
1414
# Show full error reports.
1515
config.consider_all_requests_local = true
1616

17+
# Enable server timing
18+
config.server_timing = true
19+
1720
# Enable/disable caching. By default caching is disabled.
1821
# Run rails dev:cache to toggle caching.
1922
if Rails.root.join("tmp/caching-dev.txt").exist?

railties/test/application/middleware_test.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def app
2121

2222
test "default middleware stack" do
2323
add_to_config "config.active_record.migration_error = :page_load"
24+
add_to_config "config.server_timing = true"
2425

2526
boot!
2627

@@ -63,6 +64,7 @@ def app
6364
"Rack::Sendfile",
6465
"ActionDispatch::Static",
6566
"ActionDispatch::Executor",
67+
"ActionDispatch::ServerTiming",
6668
"ActiveSupport::Cache::Strategy::LocalCache",
6769
"Rack::Runtime",
6870
"Rack::MethodOverride",

0 commit comments

Comments
 (0)