├── Rakefile ├── lib ├── server_timing │ ├── version.rb │ ├── railtie.rb │ ├── store.rb │ ├── middleware.rb │ ├── timing_metric.rb │ ├── auth.rb │ └── response_manipulator.rb └── server_timing.rb ├── Gemfile ├── .gitignore ├── bin ├── setup └── console ├── CHANGELOG.md ├── server_timing.gemspec ├── LICENSE.txt └── README.md /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | task :default => :spec 3 | -------------------------------------------------------------------------------- /lib/server_timing/version.rb: -------------------------------------------------------------------------------- 1 | module ServerTiming 2 | VERSION = "1.0.2" 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in server_timing.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/server_timing/railtie.rb: -------------------------------------------------------------------------------- 1 | module ServerTiming 2 | class Railtie < Rails::Railtie 3 | initializer "server_timing.configure_rails_initialization" do |app| 4 | app.middleware.use ServerTiming::Middleware 5 | end 6 | end 7 | end -------------------------------------------------------------------------------- /lib/server_timing/store.rb: -------------------------------------------------------------------------------- 1 | module ServerTiming 2 | class Store 3 | attr_reader :metrics 4 | 5 | def initialize 6 | end 7 | 8 | def track!(metrics, options={}) 9 | @metrics = metrics 10 | end 11 | end 12 | end -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.0.2 4 | 5 | * Switching to double-quoted header values ([#7](https://github.com/scoutapp/ruby_server_timing/pull/7)) 6 | 7 | ## 1.0.1 8 | 9 | * Fix when `scout_apm` isn't installed separately 10 | 11 | ## 1.0.0 12 | 13 | 🚀 -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "server_timing" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /lib/server_timing.rb: -------------------------------------------------------------------------------- 1 | module ServerTiming 2 | def self.rails? 3 | defined? Rails::Railtie 4 | end 5 | end 6 | 7 | require "scout_apm" 8 | 9 | require "server_timing/auth" 10 | require "server_timing/middleware" 11 | require "server_timing/railtie" if ServerTiming.rails? 12 | require "server_timing/response_manipulator" 13 | require "server_timing/store" 14 | require "server_timing/timing_metric" 15 | require "server_timing/version" -------------------------------------------------------------------------------- /lib/server_timing/middleware.rb: -------------------------------------------------------------------------------- 1 | module ServerTiming 2 | class Middleware 3 | def initialize(app) 4 | @app = app 5 | end 6 | 7 | def call(env) 8 | rack_response = @app.call(env) 9 | begin 10 | ResponseManipulator.new(env, rack_response).call 11 | rescue Exception => e 12 | # If anything went wrong at all, just bail out and return the unmodified response. 13 | puts "ServerTiming raised an exception: #{e.message}, #{e.backtrace}" 14 | rack_response 15 | ensure 16 | # Reset auth after each request 17 | ServerTiming::Auth.reset! 18 | end 19 | 20 | end 21 | end 22 | end -------------------------------------------------------------------------------- /lib/server_timing/timing_metric.rb: -------------------------------------------------------------------------------- 1 | module ServerTiming 2 | # Encapsulates a single metric that should be sent inside the server timing response header. 3 | class TimingMetric 4 | attr_reader :name 5 | attr_reader :duration 6 | attr_reader :description 7 | 8 | def self.from_scout(meta,stats) 9 | name = meta.type 10 | duration = stats.total_exclusive_time*1000 11 | new(name, duration) 12 | end 13 | 14 | def initialize(name,duration, description: nil) 15 | @name = name 16 | @duration = duration 17 | @description = description 18 | end 19 | 20 | def to_header 21 | "#{name}; dur=#{duration.to_d.truncate(2).to_f}; #{description_to_header}" 22 | end 23 | 24 | def description_to_header 25 | return unless description 26 | "desc=\"#{description}\";" 27 | end 28 | end 29 | end -------------------------------------------------------------------------------- /server_timing.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'server_timing/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "server_timing" 8 | spec.version = ServerTiming::VERSION 9 | spec.authors = ["Derek Haynes"] 10 | spec.email = ["derek.haynes@gmail.com"] 11 | 12 | spec.summary = %q{View server-side performance metrics in your browser.} 13 | spec.homepage = "https://github.com/scoutapp/ruby_server_timing" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 17 | spec.bindir = "exe" 18 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_development_dependency "bundler", "~> 1.11" 22 | spec.add_development_dependency "rake", "~> 10.0" 23 | 24 | spec.add_runtime_dependency "scout_apm" 25 | end 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Derek Haynes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/server_timing/auth.rb: -------------------------------------------------------------------------------- 1 | module ServerTiming 2 | # Encapsulates logic that determines whether the user is properly authorized to view server timing response headers. 3 | class Auth 4 | def self.ok! 5 | self.state=true 6 | end 7 | 8 | def self.deny! 9 | self.state=false 10 | end 11 | 12 | def self.reset! 13 | self.state=nil 14 | end 15 | 16 | def self.state=(new_state) 17 | Thread.current[:server_timing_authorized] = new_state 18 | end 19 | 20 | # Can be one of three values: 21 | # * true 22 | # * false 23 | # * nil (default) 24 | def self.state 25 | Thread.current[:server_timing_authorized] 26 | end 27 | 28 | def self.permitted? 29 | if state 30 | return true 31 | elsif state.is_a?(FalseClass) 32 | return false 33 | else # implied access - state has not been set 34 | # If not Rails, return true 35 | return true if !ServerTiming.rails? 36 | 37 | # If in a non-production environment, permit 38 | return true if !Rails.env.production? 39 | 40 | # In production, return false if no state has been set 41 | return false if Rails.env.production? 42 | end 43 | 44 | end 45 | end 46 | end -------------------------------------------------------------------------------- /lib/server_timing/response_manipulator.rb: -------------------------------------------------------------------------------- 1 | module ServerTiming 2 | # Adds the 'Server-Timing' response header w/metrics from Scout. 3 | class ResponseManipulator 4 | attr_reader :rack_response 5 | attr_reader :rack_status, :rack_headers, :rack_body 6 | attr_reader :env 7 | 8 | def initialize(env, rack_response) 9 | @env = env 10 | @rack_response = rack_response 11 | 12 | @rack_status = rack_response[0] 13 | @rack_headers = rack_response[1] 14 | @rack_body = rack_response[2] 15 | end 16 | 17 | def call 18 | return rack_response unless preconditions_met? 19 | 20 | store_metrics 21 | add_header 22 | rebuild_rack_response 23 | end 24 | 25 | # Checks if we should attempt to gather metrics. 26 | def preconditions_met? 27 | # tracked_request.root_layer is nil for Rack apps ... unsure why. 28 | return false unless tracked_request.root_layer 29 | 30 | Auth.permitted? 31 | end 32 | 33 | def add_header 34 | rack_headers['Server-Timing'] = payload 35 | end 36 | 37 | def tracked_request 38 | @tracked_request ||= ScoutApm::RequestManager.lookup 39 | end 40 | 41 | def store 42 | @store ||= ServerTiming::Store.new 43 | end 44 | 45 | def store_metrics 46 | layer_finder = ScoutApm::LayerConverters::FindLayerByType.new(tracked_request) 47 | converters = [ScoutApm::LayerConverters::MetricConverter] 48 | 49 | walker = ScoutApm::LayerConverters::DepthFirstWalker.new(tracked_request.root_layer) 50 | converters = converters.map do |klass| 51 | instance = klass.new(ScoutApm::Agent.instance.context, tracked_request, layer_finder, store) 52 | instance.register_hooks(walker) 53 | instance 54 | end 55 | walker.walk 56 | converters.each {|i| i.record! } 57 | end 58 | 59 | def server_timing_metrics 60 | @server_timing_metrics ||= store.metrics.map { |meta, stats| TimingMetric.from_scout(meta,stats)} 61 | end 62 | 63 | 64 | def payload 65 | headers = server_timing_metrics.map(&:to_header) 66 | headers << TimingMetric.new('Total', server_timing_metrics.map(&:duration).reduce(0,:+)).to_header 67 | headers.join(",") 68 | end 69 | 70 | def rebuild_rack_response 71 | [rack_status, rack_headers, rack_body] 72 | end 73 | end 74 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Server Timing Response Headers for Rails 2 | 3 | Bring Ruby on Rails server-side performance metrics 📈 to Chrome's Developer Tools (and other browsers that support the [Server Timing API](https://w3c.github.io/server-timing/)) via the `server_timing` gem. Production Safe™. 4 | 5 | Metrics are collected from the [scout_apm](https://github.com/scoutapp/scout_apm_ruby) gem. A [Scout](https://scoutapp.com) account is not required. 6 | 7 | ![server timing screenshot](https://s3-us-west-1.amazonaws.com/scout-blog/ruby_server_timing.png?x) 8 | 9 | ## Gem Installation 10 | 11 | Add this line to your application's Gemfile: 12 | 13 | ```ruby 14 | gem 'server_timing' 15 | ``` 16 | 17 | And then execute: 18 | 19 | $ bundle 20 | 21 | ## Configuration 22 | 23 | A minimal Scout config file is required. The `server_timing` gem reports metrics collected by the [scout_apm](https://github.com/scoutapp/scout_apm_ruby) gem (added as a dependency of `server_timing`). 24 | 25 | If you don't have a Scout account, copy and paste the following minimal configuration into a `RAILS_ROOT/config/scout_apm.yml` file: 26 | 27 | ```yaml 28 | common: &defaults 29 | monitor: true 30 | 31 | production: 32 | <<: *defaults 33 | ``` 34 | 35 | If you have a Scout account, no extra configuration is required. If you wish to see server timing metrics in development, ensure `monitor: true` is set for the `development` environment in the `scout_apm.yml` file. 36 | 37 | [See the scout_apm configuration reference](http://help.apm.scoutapp.com/#ruby-configuration-options) for more information. 38 | 39 | ## Browser Support 40 | 41 | - Chrome 65+ (Chrome 64 uses an [old format](https://github.com/scoutapp/ruby_server_timing/issues/5#issuecomment-370504687) of the server timing headers. This isn't supported by the gem). 42 | - Firefox 59+ 43 | - Opera 52+ 44 | 45 | ## Instrumentation 46 | 47 | ### Auto-Instrumentation 48 | 49 | By default, the total time consumed by each of the libraries `scout_apm` instruments is reported. This includes ActiveRecord, HTTP, Redis, and more. [View the full list of supported libraries](http://help.apm.scoutapp.com/#ruby-instrumented-libs). 50 | 51 | ### Custom Instrumentation 52 | 53 | Collect performance data on additional method calls by adding custom instrumentation via `scout_apm`. [See the docs for instructions](http://help.apm.scoutapp.com/#ruby-custom-instrumentation). 54 | 55 | ## Security 56 | 57 | * Non-Production Environments (ex: development, staging) - Server timing response headers are sent by default. 58 | * Production - The headers must be enabled. 59 | 60 | Response headers can be enabled in production by calling `ServerTiming::Auth.ok!`: 61 | 62 | ```ruby 63 | # app/controllers/application_controller.rb 64 | 65 | before_action do 66 | if current_user && current_user.admin? 67 | ServerTiming::Auth.ok! 68 | end 69 | end 70 | ``` 71 | 72 | To only enable response headers in development and for admins in production: 73 | 74 | ```ruby 75 | # app/controllers/application_controller.rb 76 | 77 | before_action do 78 | if current_user && current_user.admin? 79 | ServerTiming::Auth.ok! 80 | elsif Rails.env.development? 81 | ServerTiming::Auth.ok! 82 | else 83 | ServerTiming::Auth.deny! 84 | end 85 | end 86 | ``` 87 | 88 | ## Overhead 89 | 90 | The `scout_apm` gem, a dependency of `server_timing`, applies [low overhead instrumentation](http://blog.scoutapp.com/articles/2016/02/07/overhead-benchmarks-new-relic-vs-scout) designed for production use. 91 | ## Development 92 | 93 | After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 94 | 95 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 96 | 97 | ## Contributing 98 | 99 | Bug reports and pull requests are welcome on GitHub at https://github.com/scoutapp/ruby_server_timing. 100 | 101 | 102 | ## License 103 | 104 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 105 | 106 | --------------------------------------------------------------------------------