├── test ├── rails_app │ ├── lib │ │ ├── tasks │ │ │ └── .gitkeep │ │ └── assets │ │ │ └── .gitkeep │ ├── public │ │ ├── favicon.ico │ │ ├── robots.txt │ │ ├── 500.html │ │ ├── 422.html │ │ ├── 404.html │ │ └── index.html │ ├── app │ │ ├── mailers │ │ │ ├── .gitkeep │ │ │ └── post_mailer.rb │ │ ├── models │ │ │ ├── .gitkeep │ │ │ └── post.rb │ │ ├── views │ │ │ ├── posts │ │ │ │ ├── _post.html.erb │ │ │ │ └── index.html.erb │ │ │ ├── post_mailer │ │ │ │ └── created.text.erb │ │ │ ├── admin │ │ │ │ └── posts │ │ │ │ │ └── index.html.erb │ │ │ └── layouts │ │ │ │ └── application.html.erb │ │ ├── helpers │ │ │ └── application_helper.rb │ │ ├── assets │ │ │ ├── images │ │ │ │ └── rails.png │ │ │ ├── stylesheets │ │ │ │ └── application.css │ │ │ └── javascripts │ │ │ │ └── application.js │ │ ├── controllers │ │ │ ├── application_controller.rb │ │ │ ├── admin │ │ │ │ └── posts_controller.rb │ │ │ └── posts_controller.rb │ │ └── jobs │ │ │ └── spam_detector_job.rb │ ├── config │ │ ├── initializers │ │ │ ├── force_test_schema_load.rb │ │ │ ├── mime_types.rb │ │ │ ├── wrap_parameters.rb │ │ │ ├── backtrace_silencers.rb │ │ │ ├── session_store.rb │ │ │ ├── secret_token.rb │ │ │ └── inflections.rb │ │ ├── database.yml │ │ ├── environment.rb │ │ ├── boot.rb │ │ ├── locales │ │ │ └── en.yml │ │ ├── secrets.yml │ │ ├── routes.rb │ │ ├── environments │ │ │ ├── development.rb │ │ │ ├── test.rb │ │ │ └── production.rb │ │ └── application.rb │ ├── config.ru │ ├── db │ │ ├── migrate │ │ │ └── 20130417154459_create_posts.rb │ │ ├── seeds.rb │ │ └── schema.rb │ ├── Rakefile │ ├── script │ │ └── rails │ └── .gitignore ├── nunes_test.rb ├── helper.rb ├── adapters │ └── timing_aliased_test.rb ├── view_instrumentation_test.rb ├── job_instrumentation_test.rb ├── mailer_instrumentation_test.rb ├── fake_udp_socket_test.rb ├── support │ ├── fake_udp_socket.rb │ └── adapter_test_helpers.rb ├── subscriber_test.rb ├── model_instrumentation_test.rb ├── namespaced_controller_instrumentation_test.rb ├── cache_instrumentation_test.rb ├── controller_instrumentation_test.rb ├── adapter_test.rb └── instrumentable_test.rb ├── lib ├── nunes │ ├── version.rb │ ├── subscribers │ │ ├── nunes.rb │ │ ├── active_job.rb │ │ ├── action_mailer.rb │ │ ├── active_record.rb │ │ ├── action_view.rb │ │ ├── active_support.rb │ │ └── action_controller.rb │ ├── adapters │ │ ├── timing_aliased.rb │ │ └── memory.rb │ ├── subscriber.rb │ ├── adapter.rb │ └── instrumentable.rb └── nunes.rb ├── .travis.yml ├── .gitignore ├── Gemfile ├── script ├── bootstrap ├── test ├── watch ├── release └── bench.rb ├── nunes.gemspec ├── Changelog.md ├── LICENSE.txt └── README.md /test/rails_app/lib/tasks/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/rails_app/public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/rails_app/app/mailers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/rails_app/app/models/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/rails_app/lib/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/rails_app/app/views/posts/_post.html.erb: -------------------------------------------------------------------------------- 1 | <%= post.title %> 2 | -------------------------------------------------------------------------------- /lib/nunes/version.rb: -------------------------------------------------------------------------------- 1 | module Nunes 2 | VERSION = "0.4.0" 3 | end 4 | -------------------------------------------------------------------------------- /test/rails_app/app/models/post.rb: -------------------------------------------------------------------------------- 1 | class Post < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /test/rails_app/app/views/post_mailer/created.text.erb: -------------------------------------------------------------------------------- 1 | PostMailer#created. 2 | -------------------------------------------------------------------------------- /test/rails_app/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /test/rails_app/app/assets/images/rails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DV/nunes/master/test/rails_app/app/assets/images/rails.png -------------------------------------------------------------------------------- /test/rails_app/config/initializers/force_test_schema_load.rb: -------------------------------------------------------------------------------- 1 | if Rails.env.test? 2 | load "#{Rails.root}/db/schema.rb" 3 | end 4 | -------------------------------------------------------------------------------- /test/rails_app/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery 3 | end 4 | -------------------------------------------------------------------------------- /test/rails_app/app/views/posts/index.html.erb: -------------------------------------------------------------------------------- 1 |

Posts

2 | 3 | <% @posts.each do |post| %> 4 | <%= render 'post', post: post %> 5 | <% end %> 6 | -------------------------------------------------------------------------------- /test/rails_app/app/views/admin/posts/index.html.erb: -------------------------------------------------------------------------------- 1 |

Admin Posts

2 | 3 | <% @posts.each do |post| %> 4 | <%= render 'posts/post', post: post %> 5 | <% end %> 6 | -------------------------------------------------------------------------------- /test/rails_app/config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: sqlite3 3 | database: db/development.sqlite3 4 | test: 5 | adapter: sqlite3 6 | database: db/test.sqlite3 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.3 4 | - 2.1.0 5 | - 2.2.0 6 | notifications: 7 | email: false 8 | bundler_args: --without guard 9 | script: script/test 10 | -------------------------------------------------------------------------------- /test/rails_app/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run RailsApp::Application 5 | -------------------------------------------------------------------------------- /test/rails_app/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | RailsApp::Application.initialize! 6 | -------------------------------------------------------------------------------- /test/rails_app/db/migrate/20130417154459_create_posts.rb: -------------------------------------------------------------------------------- 1 | class CreatePosts < ActiveRecord::Migration 2 | def change 3 | create_table :posts do |t| 4 | t.string :title 5 | t.timestamps 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/rails_app/config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | # Set up gems listed in the Gemfile. 4 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 5 | 6 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) 7 | -------------------------------------------------------------------------------- /test/rails_app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-Agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | *.swp 4 | .bundle 5 | .config 6 | .yardoc 7 | Gemfile.lock 8 | InstalledFiles 9 | _yardoc 10 | coverage 11 | doc/ 12 | lib/bundler/man 13 | pkg 14 | rdoc 15 | spec/reports 16 | test/tmp 17 | test/version_tmp 18 | tmp 19 | -------------------------------------------------------------------------------- /test/rails_app/app/jobs/spam_detector_job.rb: -------------------------------------------------------------------------------- 1 | class SpamDetectorJob < ActiveJob::Base 2 | queue_as :default 3 | 4 | def perform(*posts) 5 | posts.detect do |post| 6 | post.title.include?("Buy watches cheap!") 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/rails_app/app/mailers/post_mailer.rb: -------------------------------------------------------------------------------- 1 | class PostMailer < ActionMailer::Base 2 | default from: "from@example.com" 3 | 4 | def created 5 | mail to: "to@example.org" 6 | end 7 | 8 | def receive(email) 9 | # do something 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/rails_app/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /test/rails_app/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | -------------------------------------------------------------------------------- /test/rails_app/Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | # Add your own tasks in files placed in lib/tasks ending in .rake, 3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 4 | 5 | require File.expand_path('../config/application', __FILE__) 6 | 7 | RailsApp::Application.load_tasks 8 | -------------------------------------------------------------------------------- /test/rails_app/script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | APP_PATH = File.expand_path('../../config/application', __FILE__) 5 | require File.expand_path('../../config/boot', __FILE__) 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /test/rails_app/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | RailsApp 5 | <%= stylesheet_link_tag "application", :media => "all" %> 6 | <%= javascript_include_tag "application" %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gemspec 3 | 4 | gem "rails", "~> 4.2.0" 5 | gem "sqlite3", "~> 1.3.7" 6 | gem "minitest", "~> 5.1" 7 | gem "rake", "~> 10.0.4" 8 | gem "test-unit", "~> 3.0" 9 | 10 | group :watch do 11 | gem "rb-fsevent", "~> 0.9.3", require: false 12 | end 13 | 14 | group :bench do 15 | gem "rblineprof", "~> 0.3.6" 16 | end 17 | -------------------------------------------------------------------------------- /test/rails_app/config/secrets.yml: -------------------------------------------------------------------------------- 1 | development: 2 | secret_key_base: "55277c259b087b9be1cee9d5c9642c556cda844ee585efb92096c97db51bd4cbe13f3fcdee4f8b0be8488da3e2a754c8" 3 | test: 4 | secret_key_base: "2e4c45cf5354a05621ffcc695b07c8d86716eb509883a4f1ec84644786b42a43cabbba096c23bf0c103de546d495428e" 5 | 6 | production: 7 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 8 | 9 | -------------------------------------------------------------------------------- /test/rails_app/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) 7 | # Mayor.create(name: 'Emanuel', city: cities.first) 8 | -------------------------------------------------------------------------------- /test/rails_app/config/routes.rb: -------------------------------------------------------------------------------- 1 | RailsApp::Application.routes.draw do 2 | namespace :admin do 3 | resources :posts, only: [:index, :new] 4 | end 5 | 6 | resources :posts, only: :index 7 | 8 | get "/some-data", to: "posts#some_data" 9 | get "/some-file", to: "posts#some_file" 10 | get "/some-redirect", to: "posts#some_redirect" 11 | get "/some-boom", to: "posts#some_boom" 12 | end 13 | -------------------------------------------------------------------------------- /test/rails_app/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | -------------------------------------------------------------------------------- /test/rails_app/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /test/rails_app/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | RailsApp::Application.config.session_store :cookie_store, key: '_rails_app_session' 4 | 5 | # Use the database for sessions instead of the cookie-based default, 6 | # which shouldn't be used to store highly confidential information 7 | # (create the session table with "rails generate session_migration") 8 | # RailsApp::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | #/ Usage: bootstrap [bundle options] 3 | #/ 4 | #/ Bundle install the dependencies. 5 | #/ 6 | #/ Examples: 7 | #/ 8 | #/ bootstrap 9 | #/ bootstrap --local 10 | #/ 11 | 12 | set -e 13 | cd $(dirname "$0")/.. 14 | 15 | [ "$1" = "--help" -o "$1" = "-h" -o "$1" = "help" ] && { 16 | grep '^#/' <"$0"| cut -c4- 17 | exit 0 18 | } 19 | 20 | rm -rf .bundle/{binstubs,config} 21 | bundle install --binstubs .bundle/binstubs --path .bundle --quiet "$@" 22 | -------------------------------------------------------------------------------- /test/rails_app/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile ~/.gitignore_global 6 | 7 | # Ignore bundler config 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | 13 | # Ignore all logfiles and tempfiles. 14 | /log 15 | /tmp 16 | -------------------------------------------------------------------------------- /test/rails_app/app/controllers/admin/posts_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::PostsController < ApplicationController 2 | # Use fake post for controller as I don't want active record to mingle here. 3 | Post = Struct.new(:title) 4 | 5 | def index 6 | @posts = [ 7 | Post.new('First'), 8 | Post.new('Second'), 9 | ] 10 | respond_to do |format| 11 | format.html { render } 12 | format.json { render :json => @posts } 13 | end 14 | end 15 | 16 | def new 17 | head :forbidden 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/rails_app/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | RailsApp::Application.config.secret_token = '2fd43a84cfa0c438b9edeb60d26a1da2d511cb861a98a0c301732f764a91d0c6c2d15c5de101e6b03bb5778498b106b99c9f912fd2067ee2ff61e212f0aa7a6c' 8 | -------------------------------------------------------------------------------- /test/nunes_test.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | class NunesTest < ActiveSupport::TestCase 4 | test "subscribe" do 5 | begin 6 | subscribers = Nunes.subscribe(adapter) 7 | assert_instance_of Array, subscribers 8 | 9 | subscribers.each do |subscriber| 10 | assert_instance_of \ 11 | ActiveSupport::Notifications::Fanout::Subscribers::Timed, 12 | subscriber 13 | end 14 | ensure 15 | Array(subscribers).each do |subscriber| 16 | ActiveSupport::Notifications.unsubscribe(subscriber) 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/nunes/subscribers/nunes.rb: -------------------------------------------------------------------------------- 1 | require "nunes/subscriber" 2 | 3 | module Nunes 4 | module Subscribers 5 | class Nunes < ::Nunes::Subscriber 6 | # Private 7 | Pattern = /\.nunes\Z/ 8 | 9 | # Private: The namespace for events to subscribe to. 10 | def self.pattern 11 | Pattern 12 | end 13 | 14 | def instrument_method_time(start, ending, transaction_id, payload) 15 | runtime = ((ending - start) * 1_000).round 16 | metric = payload[:metric] 17 | 18 | timing "#{metric}", runtime if metric 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] = "test" 2 | 3 | require "rails" 4 | require "rails/test_help" 5 | require "action_mailer" 6 | 7 | # require everything in the support directory 8 | root = Pathname(__FILE__).dirname.join("..").expand_path 9 | Dir[root.join("test/support/**/*.rb")].each { |f| require f } 10 | 11 | class ActionController::TestCase 12 | include AdapterTestHelpers 13 | end 14 | 15 | class ActionMailer::TestCase 16 | include AdapterTestHelpers 17 | end 18 | 19 | class ActiveSupport::TestCase 20 | include AdapterTestHelpers 21 | end 22 | 23 | require "rails_app/config/environment" 24 | 25 | require "nunes" 26 | -------------------------------------------------------------------------------- /test/rails_app/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | # 12 | # These inflection rules are supported but not enabled by default: 13 | # ActiveSupport::Inflector.inflections do |inflect| 14 | # inflect.acronym 'RESTful' 15 | # end 16 | -------------------------------------------------------------------------------- /test/rails_app/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the top of the 9 | * compiled file, but it's generally better to create a new file per style scope. 10 | * 11 | *= require_self 12 | *= require_tree . 13 | */ 14 | -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | #/ Usage: test [individual test file] 3 | #/ 4 | #/ Bootstrap and run all tests or an individual test. 5 | #/ 6 | #/ Examples: 7 | #/ 8 | #/ # run all tests 9 | #/ test 10 | #/ 11 | #/ # run individual test 12 | #/ test test/controller_instrumentation_test.rb 13 | #/ 14 | 15 | set -e 16 | cd $(dirname "$0")/.. 17 | 18 | [ "$1" = "--help" -o "$1" = "-h" -o "$1" = "help" ] && { 19 | grep '^#/' <"$0"| cut -c4- 20 | exit 0 21 | } 22 | 23 | script/bootstrap && ruby -I lib -I test -r rubygems \ 24 | -e 'require "bundler/setup"' \ 25 | -e '(ARGV.empty? ? Dir["test/**/*_test.rb"] : ARGV).each { |f| load f }' -- "$@" 26 | -------------------------------------------------------------------------------- /test/rails_app/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // the compiled file. 9 | // 10 | // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD 11 | // GO AFTER THE REQUIRES BELOW. 12 | // 13 | //= require_tree . 14 | -------------------------------------------------------------------------------- /script/watch: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | #/ Usage: watch 3 | #/ 4 | #/ Run the tests whenever any relevant files change. 5 | #/ 6 | 7 | require "pathname" 8 | require "rubygems" 9 | require "bundler" 10 | Bundler.setup :watch 11 | 12 | # Put us where we belong, in the root dir of the project. 13 | Dir.chdir Pathname.new(__FILE__).realpath + "../.." 14 | 15 | # Run the tests to start. 16 | system "clear; script/test" 17 | 18 | require "rb-fsevent" 19 | 20 | IgnoreRegex = /\/log|db/ 21 | 22 | fs = FSEvent.new 23 | fs.watch ["lib", "test"], latency: 1 do |args| 24 | unless args.first =~ IgnoreRegex 25 | system "clear" 26 | puts "#{args.first} changed..." 27 | system "script/test" 28 | end 29 | end 30 | fs.run 31 | -------------------------------------------------------------------------------- /test/rails_app/app/controllers/posts_controller.rb: -------------------------------------------------------------------------------- 1 | class PostsController < ApplicationController 2 | # Use fake post for controller as I don't want active record to mingle here. 3 | Post = Struct.new(:title) 4 | 5 | def index 6 | @posts = [ 7 | Post.new('First'), 8 | Post.new('Second'), 9 | ] 10 | end 11 | 12 | def some_data 13 | data = Rails.root.join('app', 'assets', 'images', 'rails.png').read 14 | send_data data, filename: 'rails.png', type: 'image/png' 15 | end 16 | 17 | def some_file 18 | send_file Rails.root.join('app', 'assets', 'images', 'rails.png') 19 | end 20 | 21 | def some_redirect 22 | redirect_to posts_path 23 | end 24 | 25 | def some_boom 26 | raise "boom!" 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/adapters/timing_aliased_test.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | require "minitest/mock" 3 | 4 | class TimingAliasedAdapterTest < ActiveSupport::TestCase 5 | test "passes increment along" do 6 | mock = MiniTest::Mock.new 7 | mock.expect :increment, nil, ["single", 1] 8 | mock.expect :increment, nil, ["double", 2] 9 | 10 | client = Nunes::Adapters::TimingAliased.new(mock) 11 | client.increment("single") 12 | client.increment("double", 2) 13 | 14 | mock.verify 15 | end 16 | 17 | test "sends timing to gauge" do 18 | mock = MiniTest::Mock.new 19 | mock.expect :gauge, nil, ["foo", 23] 20 | 21 | client = Nunes::Adapters::TimingAliased.new(mock) 22 | client.timing("foo", 23) 23 | 24 | mock.verify 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/rails_app/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

We're sorry, but something went wrong.

23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /lib/nunes/subscribers/active_job.rb: -------------------------------------------------------------------------------- 1 | require "nunes/subscriber" 2 | 3 | module Nunes 4 | module Subscribers 5 | class ActiveJob < ::Nunes::Subscriber 6 | # Private 7 | Pattern = /\.active_job\Z/ 8 | 9 | # Private: The namespace for events to subscribe to. 10 | def self.pattern 11 | Pattern 12 | end 13 | 14 | def perform(start, ending, transaction_id, payload) 15 | runtime = ((ending - start) * 1_000).round 16 | job = payload[:job].class.to_s.underscore 17 | 18 | timing "active_job.#{job}.perform", runtime 19 | end 20 | 21 | def enqueue(start, ending, transaction_id, payload) 22 | job = payload[:job].class.to_s.underscore 23 | increment "active_job.#{job}.enqueue" 24 | end 25 | end 26 | end 27 | end 28 | 29 | -------------------------------------------------------------------------------- /test/view_instrumentation_test.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | class ViewInstrumentationTest < ActionController::TestCase 4 | tests PostsController 5 | 6 | setup :setup_subscriber 7 | teardown :teardown_subscriber 8 | 9 | def setup_subscriber 10 | @subscriber = Nunes::Subscribers::ActionView.subscribe(adapter) 11 | end 12 | 13 | def teardown_subscriber 14 | ActiveSupport::Notifications.unsubscribe @subscriber if @subscriber 15 | end 16 | 17 | test "render_template" do 18 | get :index 19 | 20 | assert_response :success 21 | assert_timer "action_view.template.app_views_posts_index_html_erb" 22 | end 23 | 24 | test "render_partial" do 25 | get :index 26 | 27 | assert_response :success 28 | assert_timer "action_view.partial.app_views_posts_post_html_erb" 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/rails_app/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The change you wanted was rejected.

23 |

Maybe you tried to change something you didn't have access to.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /test/rails_app/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The page you were looking for doesn't exist.

23 |

You may have mistyped the address or the page may have moved.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /test/job_instrumentation_test.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | class JobInstrumentationTest < ActiveSupport::TestCase 4 | setup :setup_subscriber 5 | teardown :teardown_subscriber 6 | 7 | def setup_subscriber 8 | @subscriber = Nunes::Subscribers::ActiveJob.subscribe(adapter) 9 | end 10 | 11 | def teardown_subscriber 12 | ActiveSupport::Notifications.unsubscribe @subscriber if @subscriber 13 | end 14 | 15 | test "perform_now" do 16 | p = Post.new(title: 'Testing') 17 | SpamDetectorJob.perform_now(p) 18 | 19 | assert_timer "active_job.spam_detector_job.perform" 20 | end 21 | 22 | test "perform_later" do 23 | p = Post.create!(title: 'Testing') 24 | SpamDetectorJob.perform_later(p) 25 | 26 | assert_timer "active_job.spam_detector_job.perform" 27 | assert_counter "active_job.spam_detector_job.enqueue" 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /test/mailer_instrumentation_test.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | class MailerInstrumentationTest < ActionMailer::TestCase 4 | tests PostMailer 5 | 6 | setup :setup_subscriber 7 | teardown :teardown_subscriber 8 | 9 | def setup_subscriber 10 | @subscriber = Nunes::Subscribers::ActionMailer.subscribe(adapter) 11 | end 12 | 13 | def teardown_subscriber 14 | ActiveSupport::Notifications.unsubscribe @subscriber if @subscriber 15 | end 16 | 17 | test "deliver_now" do 18 | PostMailer.created.deliver_now 19 | assert_timer "action_mailer.deliver.PostMailer" 20 | end 21 | 22 | test "deliver_later" do 23 | PostMailer.created.deliver_later 24 | assert_timer "action_mailer.deliver.PostMailer" 25 | end 26 | 27 | test "receive" do 28 | PostMailer.receive PostMailer.created 29 | assert_timer "action_mailer.receive.PostMailer" 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/nunes/adapters/timing_aliased.rb: -------------------------------------------------------------------------------- 1 | require "nunes/adapter" 2 | 3 | module Nunes 4 | module Adapters 5 | # Internal: Adapter that aliases timing to gauge. One of the supported 6 | # places to send instrumentation data is instrumentalapp.com. Their agent 7 | # uses gauge under the hood for timing information. This adapter is used to 8 | # adapter their gauge interface to the timing one used internally in the 9 | # gem. This should never need to be used directly by a user of the gem. 10 | class TimingAliased < ::Nunes::Adapter 11 | def self.wraps?(client) 12 | client.respond_to?(:increment) && 13 | client.respond_to?(:gauge) && 14 | !client.respond_to?(:timing) 15 | end 16 | 17 | # Internal: Adapter timing to gauge. 18 | def timing(metric, duration) 19 | @client.gauge prepare(metric), duration 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/fake_udp_socket_test.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | class FakeUdpSocketTest < ActiveSupport::TestCase 4 | test "timer boolean" do 5 | socket = FakeUdpSocket.new 6 | socket.send "action_controller.posts.index.runtime:2|ms" 7 | assert_equal true, socket.timer?("action_controller.posts.index.runtime") 8 | assert_equal false, socket.timer?("action_controller.posts.index.faketime") 9 | end 10 | 11 | test "counter boolean" do 12 | socket = FakeUdpSocket.new 13 | socket.send "action_controller.status.200:1|c" 14 | assert_equal true, socket.counter?("action_controller.status.200") 15 | assert_equal false, socket.counter?("action_controller.status.400") 16 | end 17 | 18 | test "send, recv and clear" do 19 | socket = FakeUdpSocket.new 20 | socket.send "foo" 21 | socket.send "bar" 22 | assert_equal "foo", socket.recv 23 | socket.clear 24 | assert_nil socket.recv 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/nunes/subscribers/action_mailer.rb: -------------------------------------------------------------------------------- 1 | require "nunes/subscriber" 2 | 3 | module Nunes 4 | module Subscribers 5 | class ActionMailer < ::Nunes::Subscriber 6 | # Private 7 | Pattern = /\.action_mailer\Z/ 8 | 9 | # Private: The namespace for events to subscribe to. 10 | def self.pattern 11 | Pattern 12 | end 13 | 14 | def deliver(start, ending, transaction_id, payload) 15 | runtime = ((ending - start) * 1_000).round 16 | mailer = payload[:mailer] 17 | 18 | if mailer 19 | timing "action_mailer.deliver.#{mailer}", runtime 20 | end 21 | end 22 | 23 | def receive(start, ending, transaction_id, payload) 24 | runtime = ((ending - start) * 1_000).round 25 | mailer = payload[:mailer] 26 | 27 | if mailer 28 | timing "action_mailer.receive.#{mailer}", runtime 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /nunes.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'nunes/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "nunes" 8 | spec.version = Nunes::VERSION 9 | spec.authors = ["John Nunemaker"] 10 | spec.email = ["nunemaker@gmail.com"] 11 | spec.description = %q{The friendly gem that instruments everything for you, like I would if I could.} 12 | spec.summary = %q{The friendly gem that instruments everything for you, like I would if I could.} 13 | spec.homepage = "https://github.com/jnunemaker/nunes" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files`.split($/) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_development_dependency "bundler", "~> 1.3" 22 | end 23 | -------------------------------------------------------------------------------- /lib/nunes/subscribers/active_record.rb: -------------------------------------------------------------------------------- 1 | require "nunes/subscriber" 2 | 3 | module Nunes 4 | module Subscribers 5 | class ActiveRecord < ::Nunes::Subscriber 6 | # Private 7 | Pattern = /\.active_record\Z/ 8 | 9 | # Private: The namespace for events to subscribe to. 10 | def self.pattern 11 | Pattern 12 | end 13 | 14 | def sql(start, ending, transaction_id, payload) 15 | runtime = ((ending - start) * 1_000).round 16 | name = payload[:name] 17 | sql = payload[:sql].to_s.strip 18 | operation = sql.split(' ', 2).first.to_s.downcase 19 | 20 | timing "active_record.sql", runtime 21 | 22 | case operation 23 | when "begin" 24 | timing "active_record.sql.transaction_begin", runtime 25 | when "commit" 26 | timing "active_record.sql.transaction_commit", runtime 27 | else 28 | timing "active_record.sql.#{operation}", runtime 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/support/fake_udp_socket.rb: -------------------------------------------------------------------------------- 1 | class FakeUdpSocket 2 | attr_reader :buffer 3 | 4 | TimingRegex = /\:\d+\|ms\Z/ 5 | CounterRegex = /\:\d+\|c\Z/ 6 | 7 | def initialize 8 | @buffer = [] 9 | end 10 | 11 | def send(message, *rest) 12 | @buffer.push message 13 | end 14 | 15 | def recv 16 | @buffer.shift 17 | end 18 | 19 | def clear 20 | @buffer = [] 21 | end 22 | 23 | def timer_metrics 24 | @buffer.grep(TimingRegex) 25 | end 26 | 27 | def timer_metric_names 28 | timer_metrics.map { |op| op.gsub(TimingRegex, '') } 29 | end 30 | 31 | def timer?(metric) 32 | timer_metric_names.include?(metric) 33 | end 34 | 35 | def counter_metrics 36 | @buffer.grep(CounterRegex) 37 | end 38 | 39 | def counter_metric_names 40 | counter_metrics.map { |op| op.gsub(CounterRegex, '') } 41 | end 42 | 43 | def counter?(metric) 44 | counter_metric_names.include?(metric) 45 | end 46 | 47 | def inspect 48 | "" 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # 0.4.0 2 | 3 | ## Backwards Compatibility Break 4 | 5 | * Changed required Rails version to 4.2. 6 | 7 | # 0.3.0 8 | 9 | ## Backwards Compatibility Break 10 | 11 | * Cleaning action view template and partial paths before sending to adapter. Prior to this change, action view metrics looked like: action_view.template.app.views.posts.post.html.erb. They now look like: action_view.template.app_views_posts_post_html_erb. The reason is that "." is typically a namespace in most metric services, which means really deep nesting of metrics, especially for views rendered from engines in gems. This keeps shallows up the nesting. Thanks to @dewski for reporting. 12 | 13 | # 0.2.0 14 | 15 | ## Backwards Compatibility Break 16 | 17 | * No longer using the inflector to pretty up metric names. This means that when you upgrade from 0.1 and 0.2 some metrics will change names. 18 | * Namespaced several of the stats to make them easier to graph. 19 | 20 | ## Fixed 21 | 22 | * Instrumenting namespaced controllers 23 | 24 | # 0.1.0 25 | 26 | * Initial release. 27 | -------------------------------------------------------------------------------- /script/release: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | #/ Usage: release 3 | #/ 4 | #/ Tag the version in the repo and push the gem. 5 | #/ 6 | 7 | set -e 8 | cd $(dirname "$0")/.. 9 | 10 | [ "$1" = "--help" -o "$1" = "-h" -o "$1" = "help" ] && { 11 | grep '^#/' <"$0"| cut -c4- 12 | exit 0 13 | } 14 | 15 | gem_name=nunes 16 | 17 | # Build a new gem archive. 18 | rm -rf $gem_name-*.gem 19 | gem build -q $gem_name.gemspec 20 | 21 | # Make sure we're on the master branch. 22 | (git branch | grep -q '* master') || { 23 | echo "Only release from the master branch." 24 | exit 1 25 | } 26 | 27 | # Figure out what version we're releasing. 28 | tag=v`ls $gem_name-*.gem | sed "s/^$gem_name-\(.*\)\.gem$/\1/"` 29 | 30 | echo "Releasing $tag" 31 | 32 | # Make sure we haven't released this version before. 33 | git fetch -t origin 34 | 35 | (git tag -l | grep -q "$tag") && { 36 | echo "Whoops, there's already a '${tag}' tag." 37 | exit 1 38 | } 39 | 40 | # Tag it and bag it. 41 | gem push $gem_name-*.gem && git tag "$tag" && 42 | git push origin master && git push origin "$tag" 43 | -------------------------------------------------------------------------------- /test/rails_app/db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended to check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(:version => 20130417154459) do 15 | 16 | create_table "posts", :force => true do |t| 17 | t.string "title" 18 | t.datetime "created_at", :null => false 19 | t.datetime "updated_at", :null => false 20 | end 21 | 22 | end 23 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 John Nunemaker 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /test/support/adapter_test_helpers.rb: -------------------------------------------------------------------------------- 1 | module AdapterTestHelpers 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | setup :setup_memory_adapter 6 | end 7 | 8 | attr_reader :adapter 9 | 10 | def setup_memory_adapter 11 | @adapter = Nunes::Adapters::Memory.new 12 | end 13 | 14 | def assert_timer(metric) 15 | assert adapter.timer?(metric), 16 | "Expected the timer #{metric.inspect} to be included in #{adapter.timer_metric_names.inspect}, but it was not." 17 | end 18 | 19 | def assert_no_timer(metric) 20 | assert ! adapter.timer?(metric), 21 | "Expected the timer #{metric.inspect} to not be included in #{adapter.timer_metric_names.inspect}, but it was." 22 | end 23 | 24 | def assert_counter(metric) 25 | assert adapter.counter?(metric), 26 | "Expected the counter #{metric.inspect} to be included in #{adapter.counter_metric_names.inspect}, but it was not." 27 | end 28 | 29 | def assert_no_counter(metric) 30 | assert ! adapter.counter?(metric), 31 | "Expected the counter #{metric.inspect} to not be included in adapter.counter_metric_names.inspect}, but it was." 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/rails_app/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | RailsApp::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Log error messages when you accidentally call methods on nil. 10 | config.whiny_nils = true 11 | 12 | # Show full error reports and disable caching 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger 20 | config.active_support.deprecation = :log 21 | 22 | # Only use best-standards-support built into browsers 23 | config.action_dispatch.best_standards_support = :builtin 24 | 25 | 26 | # Do not compress assets 27 | config.assets.compress = false 28 | 29 | # Expands the lines which load the assets 30 | config.assets.debug = true 31 | end 32 | -------------------------------------------------------------------------------- /test/subscriber_test.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | require "minitest/mock" 3 | 4 | class SubscriberTest < ActiveSupport::TestCase 5 | attr_reader :subscriber_class 6 | 7 | setup :setup_subscriber_class 8 | 9 | def setup_subscriber_class 10 | @subscriber_class = Class.new(Nunes::Subscriber) do 11 | def self.pattern 12 | /\.test\Z/ 13 | end 14 | 15 | def foo(*args) 16 | increment "test.foo" 17 | end 18 | 19 | # minitest stub works with call, so i change it to just return self, since 20 | # all we really want to test in this instance is that things are wired 21 | # up right, not that call dispatches events correctly 22 | def call(*args) 23 | self 24 | end 25 | end 26 | end 27 | 28 | test "subscribe" do 29 | client = {} 30 | instance = subscriber_class.new(client) 31 | 32 | subscriber_class.stub :new, instance do 33 | mock = MiniTest::Mock.new 34 | 35 | mock.expect :subscribe, :subscriber, [subscriber_class.pattern, instance] 36 | 37 | assert_equal :subscriber, 38 | subscriber_class.subscribe(adapter, subscriber: mock) 39 | 40 | mock.verify 41 | end 42 | end 43 | 44 | test "initialize" do 45 | adapter = Object.new 46 | Nunes::Adapter.stub :wrap, adapter do 47 | instance = subscriber_class.new({}) 48 | assert_equal adapter, instance.adapter 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /script/bench.rb: -------------------------------------------------------------------------------- 1 | require "benchmark" 2 | require "securerandom" 3 | require "active_support/notifications" 4 | require "bundler" 5 | Bundler.setup :default, :bench 6 | 7 | require_relative "../lib/nunes" 8 | 9 | adapter = Nunes::Adapter.wrap({}) 10 | Nunes::Subscribers::ActionController.subscribe(adapter) 11 | 12 | require "rblineprof" 13 | $profile = lineprof(/./) do 14 | puts Benchmark.realtime { 15 | 1_000.times do 16 | ActiveSupport::Notifications.instrument("process_action.action_controller") 17 | end 18 | } 19 | end 20 | 21 | def show_file(file) 22 | file = File.expand_path(file) 23 | File.readlines(file).each_with_index do |line, num| 24 | wall, cpu, calls = $profile[file][num+1] 25 | if calls && calls > 0 26 | printf "% 8.1fms + % 8.1fms (% 5d) | %s", cpu/1000.0, (wall-cpu)/1000.0, calls, line 27 | # printf "% 8.1fms (% 5d) | %s", wall/1000.0, calls, line 28 | else 29 | printf " | %s", line 30 | # printf " | %s", line 31 | end 32 | end 33 | end 34 | 35 | puts 36 | $profile.each do |file, data| 37 | total, child, exclusive = data[0] 38 | puts file 39 | printf " % 10.1fms in this file\n", exclusive/1000.0 40 | printf " % 10.1fms in this file + children\n", total/1000.0 41 | printf " % 10.1fms in children\n", child/1000.0 42 | 43 | if file =~ /nunes\/lib/ 44 | show_file(file) 45 | end 46 | 47 | puts 48 | end 49 | -------------------------------------------------------------------------------- /test/model_instrumentation_test.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | class ModelInstrumentationTest < ActiveSupport::TestCase 4 | setup :setup_subscriber 5 | teardown :teardown_subscriber 6 | 7 | def setup_subscriber 8 | @subscriber = Nunes::Subscribers::ActiveRecord.subscribe(adapter) 9 | end 10 | 11 | def teardown_subscriber 12 | ActiveSupport::Notifications.unsubscribe @subscriber if @subscriber 13 | end 14 | 15 | test "transaction" do 16 | Post.create(title: 'Testing') 17 | 18 | assert_timer "active_record.sql.transaction_begin" 19 | assert_timer "active_record.sql.transaction_commit" 20 | end 21 | 22 | test "create" do 23 | Post.create(title: 'Testing') 24 | 25 | assert_timer "active_record.sql" 26 | assert_timer "active_record.sql.insert" 27 | end 28 | 29 | test "update" do 30 | post = Post.create 31 | adapter.clear 32 | post.update_attributes(title: "Title") 33 | 34 | assert_timer "active_record.sql" 35 | assert_timer "active_record.sql.update" 36 | end 37 | 38 | test "find" do 39 | post = Post.create 40 | adapter.clear 41 | Post.find(post.id) 42 | 43 | assert_timer "active_record.sql" 44 | assert_timer "active_record.sql.select" 45 | end 46 | 47 | test "destroy" do 48 | post = Post.create 49 | adapter.clear 50 | post.destroy 51 | 52 | assert_timer "active_record.sql" 53 | assert_timer "active_record.sql.delete" 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/nunes.rb: -------------------------------------------------------------------------------- 1 | require "nunes/instrumentable" 2 | 3 | require "nunes/adapters/memory" 4 | require "nunes/adapters/timing_aliased" 5 | 6 | require "nunes/subscriber" 7 | require "nunes/subscribers/action_controller" 8 | require "nunes/subscribers/action_view" 9 | require "nunes/subscribers/action_mailer" 10 | require "nunes/subscribers/active_support" 11 | require "nunes/subscribers/active_record" 12 | require "nunes/subscribers/active_job" 13 | require "nunes/subscribers/nunes" 14 | 15 | module Nunes 16 | # Public: Shortcut method to setup all subscribers for a given client. 17 | # 18 | # client - The instance that will be adapted and receive all instrumentation. 19 | # 20 | # Examples: 21 | # 22 | # Nunes.subscribe(Statsd.new) 23 | # Nunes.subscribe(Instrumental::Agent.new) 24 | # 25 | # Returns Array of subscribers that were setup. 26 | def self.subscribe(client) 27 | subscribers = [] 28 | adapter = Nunes::Adapter.wrap(client) 29 | 30 | subscribers << Subscribers::ActionController.subscribe(adapter) 31 | subscribers << Subscribers::ActionView.subscribe(adapter) 32 | subscribers << Subscribers::ActionMailer.subscribe(adapter) 33 | subscribers << Subscribers::ActiveSupport.subscribe(adapter) 34 | subscribers << Subscribers::ActiveRecord.subscribe(adapter) 35 | subscribers << Subscribers::ActiveJob.subscribe(adapter) 36 | subscribers << Subscribers::Nunes.subscribe(adapter) 37 | 38 | subscribers 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/namespaced_controller_instrumentation_test.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | class NamespacedControllerInstrumentationTest < ActionController::TestCase 4 | tests Admin::PostsController 5 | 6 | setup :setup_subscriber 7 | teardown :teardown_subscriber 8 | 9 | def setup_subscriber 10 | @subscriber = Nunes::Subscribers::ActionController.subscribe(adapter) 11 | end 12 | 13 | def teardown_subscriber 14 | ActiveSupport::Notifications.unsubscribe @subscriber if @subscriber 15 | end 16 | 17 | test "process_action" do 18 | get :index 19 | 20 | assert_response :success 21 | 22 | assert_timer "action_controller.controller.Admin.PostsController.index.runtime.total" 23 | assert_timer "action_controller.controller.Admin.PostsController.index.runtime.view" 24 | assert_timer "action_controller.controller.Admin.PostsController.index.runtime.db" 25 | 26 | assert_counter "action_controller.format.html" 27 | assert_counter "action_controller.status.200" 28 | 29 | assert_counter "action_controller.controller.Admin.PostsController.index.format.html" 30 | assert_counter "action_controller.controller.Admin.PostsController.index.status.200" 31 | end 32 | 33 | test "process_action w/ json" do 34 | get :index, format: :json 35 | 36 | assert_counter "action_controller.controller.Admin.PostsController.index.format.json" 37 | end 38 | 39 | test "process_action bad_request" do 40 | get :new 41 | 42 | assert_response :forbidden 43 | 44 | assert_counter "action_controller.controller.Admin.PostsController.new.status.403" 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/nunes/subscribers/action_view.rb: -------------------------------------------------------------------------------- 1 | require "nunes/subscriber" 2 | 3 | module Nunes 4 | module Subscribers 5 | class ActionView < ::Nunes::Subscriber 6 | # Private 7 | Pattern = /\.action_view\Z/ 8 | 9 | # Private: The namespace for events to subscribe to. 10 | def self.pattern 11 | Pattern 12 | end 13 | 14 | def render_template(start, ending, transaction_id, payload) 15 | instrument_identifier :template, payload[:identifier], start, ending 16 | end 17 | 18 | def render_partial(start, ending, transaction_id, payload) 19 | instrument_identifier :partial, payload[:identifier], start, ending 20 | end 21 | 22 | private 23 | 24 | # Private: Sends timing information about identifier event. 25 | def instrument_identifier(kind, identifier, start, ending) 26 | if identifier 27 | runtime = ((ending - start) * 1_000).round 28 | timing identifier_to_metric(kind, identifier), runtime 29 | end 30 | end 31 | 32 | # Private: What to replace file separators with. 33 | FileSeparatorReplacement = "_" 34 | 35 | # Private: Converts an identifier to a metric name. Strips out the rails 36 | # root from the full path. 37 | # 38 | # identifier - The String full path to the template or partial. 39 | def identifier_to_metric(kind, identifier) 40 | view_path = identifier.to_s.gsub(::Rails.root.to_s, "") 41 | metric = adapter.prepare(view_path, FileSeparatorReplacement) 42 | "action_view.#{kind}.#{metric}" 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/rails_app/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | RailsApp::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Configure static asset server for tests with Cache-Control for performance 11 | config.serve_static_files = true 12 | config.static_cache_control = "public, max-age=3600" 13 | 14 | # Log error messages when you accidentally call methods on nil 15 | config.whiny_nils = true 16 | config.eager_load = false 17 | 18 | # Show full error reports and disable caching 19 | config.consider_all_requests_local = true 20 | config.action_controller.perform_caching = false 21 | 22 | # Raise exceptions instead of rendering exception templates 23 | config.action_dispatch.show_exceptions = false 24 | 25 | # Disable request forgery protection in test environment 26 | config.action_controller.allow_forgery_protection = false 27 | 28 | # Tell Action Mailer not to deliver emails to the real world. 29 | # The :test delivery method accumulates sent emails in the 30 | # ActionMailer::Base.deliveries array. 31 | config.action_mailer.delivery_method = :test 32 | config.active_support.test_order = :random 33 | 34 | 35 | 36 | # Print deprecation notices to the stderr 37 | config.active_support.deprecation = :stderr 38 | end 39 | -------------------------------------------------------------------------------- /lib/nunes/adapters/memory.rb: -------------------------------------------------------------------------------- 1 | require "nunes/adapter" 2 | 3 | module Nunes 4 | module Adapters 5 | # Internal: Memory backend for recording instrumentation calls. This should 6 | # never need to be used directly by a user of the gem. 7 | class Memory < ::Nunes::Adapter 8 | def self.wraps?(client) 9 | client.is_a?(Hash) 10 | end 11 | 12 | def initialize(client = nil) 13 | @client = client || {} 14 | clear 15 | end 16 | 17 | def increment(metric, value = 1) 18 | counters << [prepare(metric), value] 19 | end 20 | 21 | def timing(metric, value) 22 | timers << [prepare(metric), value] 23 | end 24 | 25 | # Internal: Returns Array of any recorded timers with durations. 26 | def timers 27 | @client.fetch(:timers) 28 | end 29 | 30 | # Internal: Returns Array of only recorded timers. 31 | def timer_metric_names 32 | timers.map { |op| op.first } 33 | end 34 | 35 | # Internal: Returns true/false if metric has been recorded as a timer. 36 | def timer?(metric) 37 | timers.detect { |op| op.first == metric } 38 | end 39 | 40 | # Internal: Returns Array of any recorded counters with values. 41 | def counters 42 | @client.fetch(:counters) 43 | end 44 | 45 | # Internal: Returns Array of only recorded counters. 46 | def counter_metric_names 47 | counters.map { |op| op.first } 48 | end 49 | 50 | # Internal: Returns true/false if metric has been recorded as a counter. 51 | def counter?(metric) 52 | counters.detect { |op| op.first == metric } 53 | end 54 | 55 | # Internal: Empties the known counters and metrics. 56 | # 57 | # Returns nothing. 58 | def clear 59 | @client ||= {} 60 | @client.clear 61 | @client[:timers] = [] 62 | @client[:counters] = [] 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/nunes/subscribers/active_support.rb: -------------------------------------------------------------------------------- 1 | require "nunes/subscriber" 2 | 3 | module Nunes 4 | module Subscribers 5 | class ActiveSupport < ::Nunes::Subscriber 6 | # Private 7 | Pattern = /\.active_support\Z/ 8 | 9 | # Private: The namespace for events to subscribe to. 10 | def self.pattern 11 | Pattern 12 | end 13 | 14 | def cache_read(start, ending, transaction_id, payload) 15 | super_operation = payload[:super_operation] 16 | runtime = ((ending - start) * 1_000).round 17 | 18 | case super_operation 19 | when Symbol 20 | timing "active_support.cache.#{super_operation}", runtime 21 | else 22 | timing "active_support.cache.read", runtime 23 | end 24 | 25 | hit = payload[:hit] 26 | unless hit.nil? 27 | hit_type = hit ? :hit : :miss 28 | increment "active_support.cache.#{hit_type}" 29 | end 30 | end 31 | 32 | def cache_generate(start, ending, transaction_id, payload) 33 | runtime = ((ending - start) * 1_000).round 34 | timing "active_support.cache.fetch_generate", runtime 35 | end 36 | 37 | def cache_fetch_hit(start, ending, transaction_id, payload) 38 | runtime = ((ending - start) * 1_000).round 39 | timing "active_support.cache.fetch_hit", runtime 40 | end 41 | 42 | def cache_write(start, ending, transaction_id, payload) 43 | runtime = ((ending - start) * 1_000).round 44 | timing "active_support.cache.write", runtime 45 | end 46 | 47 | def cache_delete(start, ending, transaction_id, payload) 48 | runtime = ((ending - start) * 1_000).round 49 | timing "active_support.cache.delete", runtime 50 | end 51 | 52 | def cache_exist?(start, ending, transaction_id, payload) 53 | runtime = ((ending - start) * 1_000).round 54 | timing "active_support.cache.exist", runtime 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/nunes/subscriber.rb: -------------------------------------------------------------------------------- 1 | require "active_support/notifications" 2 | 3 | module Nunes 4 | class Subscriber 5 | # Private: The bang character that is the first char of some events. 6 | BANG = '!' 7 | 8 | # Public: Setup a subscription for the subscriber using the 9 | # provided adapter. 10 | # 11 | # adapter - The adapter instance to send instrumentation to. 12 | def self.subscribe(adapter, options = {}) 13 | subscriber = options.fetch(:subscriber) { ActiveSupport::Notifications } 14 | subscriber.subscribe pattern, new(adapter) 15 | end 16 | 17 | def self.pattern 18 | raise "Not Implemented, override in subclass and provide a regex or string." 19 | end 20 | 21 | # Private: The adapter to send instrumentation to. 22 | attr_reader :adapter 23 | 24 | # Internal: Initializes a new instance. 25 | # 26 | # adapter - The adapter instance to send instrumentation to. 27 | def initialize(adapter) 28 | @adapter = Nunes::Adapter.wrap(adapter) 29 | end 30 | 31 | # Private: Dispatcher that converts incoming events to method calls. 32 | def call(name, start, ending, transaction_id, payload) 33 | # rails doesn't recommend instrumenting methods that start with bang 34 | # when in production 35 | return if name.start_with?(BANG) 36 | 37 | method_name = name.split('.').first 38 | 39 | if respond_to?(method_name) 40 | send(method_name, start, ending, transaction_id, payload) 41 | end 42 | end 43 | 44 | # Internal: Increment a metric for the client. 45 | # 46 | # metric - The String name of the metric to increment. 47 | # value - The Integer value to increment by. 48 | # 49 | # Returns nothing. 50 | def increment(metric, value = 1) 51 | @adapter.increment metric, value 52 | end 53 | 54 | # Internal: Track the timing of a metric for the client. 55 | # 56 | # metric - The String name of the metric. 57 | # value - The Integer duration of the event in milliseconds. 58 | # 59 | # Returns nothing. 60 | def timing(metric, value) 61 | @adapter.timing metric, value 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/nunes/adapter.rb: -------------------------------------------------------------------------------- 1 | module Nunes 2 | class Adapter 3 | # Private: Wraps a given object with the correct adapter/decorator. 4 | # 5 | # client - The thing to be wrapped. 6 | # 7 | # Returns Nunes::Adapter instance. 8 | def self.wrap(client) 9 | raise ArgumentError, "client cannot be nil" if client.nil? 10 | return client if client.is_a?(self) 11 | 12 | adapter = adapters.detect { |adapter| adapter.wraps?(client) } 13 | 14 | if adapter.nil? 15 | raise ArgumentError, 16 | "I have no clue how to wrap what you've given me (#{client.inspect})" 17 | end 18 | 19 | adapter.new(client) 20 | end 21 | 22 | # Private 23 | def self.wraps?(client) 24 | client.respond_to?(:increment) && client.respond_to?(:timing) 25 | end 26 | 27 | # Private 28 | def self.adapters 29 | [Nunes::Adapter, *subclasses] 30 | end 31 | 32 | # Private 33 | attr_reader :client 34 | 35 | # Internal: Sets the client for the adapter. 36 | # 37 | # client - The thing being adapted to a simple interface. 38 | def initialize(client) 39 | @client = client 40 | end 41 | 42 | # Internal: Increment a metric by a value. Override in subclass if client 43 | # interface does not match. 44 | def increment(metric, value = 1) 45 | @client.increment prepare(metric), value 46 | end 47 | 48 | # Internal: Record a metric's duration. Override in subclass if client 49 | # interface does not match. 50 | def timing(metric, duration) 51 | @client.timing prepare(metric), duration 52 | end 53 | 54 | # Private: What Ruby uses to separate namespaces. 55 | ReplaceRegex = /[^a-z0-9\-_]+/i 56 | 57 | # Private: The default metric namespace separator. 58 | Separator = "." 59 | 60 | # Private 61 | Nothing = "" 62 | 63 | # Private: Prepare a metric name before it is sent to the adapter's client. 64 | def prepare(metric, replacement = Separator) 65 | escaped = Regexp.escape(replacement) 66 | replace_begin_end_regex = /\A#{escaped}|#{escaped}\Z/ 67 | 68 | metric = metric.to_s.gsub(ReplaceRegex, replacement) 69 | metric.squeeze!(replacement) 70 | metric.gsub!(replace_begin_end_regex, Nothing) 71 | metric 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /test/cache_instrumentation_test.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | class CacheInstrumentationTest < ActiveSupport::TestCase 4 | attr_reader :cache 5 | 6 | setup :setup_subscriber, :setup_cache 7 | teardown :teardown_subscriber, :teardown_cache 8 | 9 | def setup_subscriber 10 | @subscriber = Nunes::Subscribers::ActiveSupport.subscribe(adapter) 11 | end 12 | 13 | def teardown_subscriber 14 | ActiveSupport::Notifications.unsubscribe @subscriber if @subscriber 15 | end 16 | 17 | def setup_cache 18 | # Deprecated in Rails 4.2 19 | # ActiveSupport::Cache::MemoryStore.instrument = true 20 | @cache = ActiveSupport::Cache::MemoryStore.new 21 | end 22 | 23 | def teardown_cache 24 | # Deprecated in Rails 4.2 25 | # ActiveSupport::Cache::MemoryStore.instrument = nil 26 | @cache = nil 27 | end 28 | 29 | test "cache_read miss" do 30 | cache.read('foo') 31 | 32 | assert_timer "active_support.cache.read" 33 | assert_counter "active_support.cache.miss" 34 | end 35 | 36 | test "cache_read hit" do 37 | cache.write('foo', 'bar') 38 | adapter.clear 39 | cache.read('foo') 40 | 41 | assert_timer "active_support.cache.read" 42 | assert_counter "active_support.cache.hit" 43 | end 44 | 45 | test "cache_generate" do 46 | cache.fetch('foo') { |key| :generate_me_please } 47 | assert_timer "active_support.cache.fetch_generate" 48 | end 49 | 50 | test "cache_fetch with hit" do 51 | cache.write('foo', 'bar') 52 | adapter.clear 53 | cache.fetch('foo') { |key| :never_gets_here } 54 | 55 | assert_timer "active_support.cache.fetch" 56 | assert_timer "active_support.cache.fetch_hit" 57 | end 58 | 59 | test "cache_fetch with miss" do 60 | cache.fetch('foo') { 'foo value set here' } 61 | 62 | assert_timer "active_support.cache.fetch" 63 | assert_timer "active_support.cache.fetch_generate" 64 | assert_timer "active_support.cache.write" 65 | end 66 | 67 | test "cache_write" do 68 | cache.write('foo', 'bar') 69 | assert_timer "active_support.cache.write" 70 | end 71 | 72 | test "cache_delete" do 73 | cache.delete('foo') 74 | assert_timer "active_support.cache.delete" 75 | end 76 | 77 | test "cache_exist?" do 78 | cache.exist?('foo') 79 | assert_timer "active_support.cache.exist" 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/nunes/instrumentable.rb: -------------------------------------------------------------------------------- 1 | require "active_support/notifications" 2 | 3 | module Nunes 4 | module Instrumentable 5 | # Private 6 | MethodTimeEventName = "instrument_method_time.nunes".freeze 7 | 8 | # Public: Instrument a method's timing by name. 9 | # 10 | # method_name - The String or Symbol name of the method. 11 | # options_or_string - The Hash of options or the String metic name. 12 | # :payload - Any items you would like to include with the 13 | # instrumentation payload. 14 | # :name - The String name of the event and namespace. 15 | def instrument_method_time(method_name, options_or_string = nil) 16 | options = options_or_string || {} 17 | options = {name: options} if options.is_a?(String) 18 | 19 | action = :time 20 | payload = options.fetch(:payload) { {} } 21 | instrumenter = options.fetch(:instrumenter) { ActiveSupport::Notifications } 22 | 23 | payload[:metric] = options.fetch(:name) { 24 | if name.nil? 25 | raise ArgumentError, "For class methods you must provide the full name of the metric." 26 | else 27 | "#{name}.#{method_name}" 28 | end 29 | } 30 | 31 | nunes_wrap_method(method_name, action) do |old_method_name, new_method_name| 32 | define_method(new_method_name) do |*args, &block| 33 | instrumenter.instrument(MethodTimeEventName, payload) { 34 | send(old_method_name, *args, &block) 35 | } 36 | end 37 | end 38 | end 39 | 40 | # Private: And so horrendously ugly... 41 | def nunes_wrap_method(method_name, action, &block) 42 | method_without_instrumentation = :"#{method_name}_without_#{action}" 43 | method_with_instrumentation = :"#{method_name}_with_#{action}" 44 | 45 | if method_defined?(method_without_instrumentation) 46 | raise ArgumentError, "already instrumented #{method_name} for #{self.name}" 47 | end 48 | 49 | if !method_defined?(method_name) && !private_method_defined?(method_name) 50 | raise ArgumentError, "could not find method #{method_name} for #{self.name}" 51 | end 52 | 53 | alias_method method_without_instrumentation, method_name 54 | yield method_without_instrumentation, method_with_instrumentation 55 | alias_method method_name, method_with_instrumentation 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /test/rails_app/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | RailsApp::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # Code is not reloaded between requests 5 | config.cache_classes = true 6 | 7 | # Full error reports are disabled and caching is turned on 8 | config.consider_all_requests_local = false 9 | config.action_controller.perform_caching = true 10 | 11 | # Disable Rails's static asset server (Apache or nginx will already do this) 12 | config.serve_static_assets = false 13 | 14 | # Compress JavaScripts and CSS 15 | config.assets.compress = true 16 | 17 | # Don't fallback to assets pipeline if a precompiled asset is missed 18 | config.assets.compile = false 19 | 20 | # Generate digests for assets URLs 21 | config.assets.digest = true 22 | 23 | # Defaults to nil and saved in location specified by config.assets.prefix 24 | # config.assets.manifest = YOUR_PATH 25 | 26 | # Specifies the header that your server uses for sending files 27 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 28 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 29 | 30 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 31 | # config.force_ssl = true 32 | 33 | # See everything in the log (default is :info) 34 | # config.log_level = :debug 35 | 36 | # Prepend all log lines with the following tags 37 | # config.log_tags = [ :subdomain, :uuid ] 38 | 39 | # Use a different logger for distributed setups 40 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 41 | 42 | # Use a different cache store in production 43 | # config.cache_store = :mem_cache_store 44 | 45 | # Enable serving of images, stylesheets, and JavaScripts from an asset server 46 | # config.action_controller.asset_host = "http://assets.example.com" 47 | 48 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) 49 | # config.assets.precompile += %w( search.js ) 50 | 51 | # Disable delivery errors, bad email addresses will be ignored 52 | # config.action_mailer.raise_delivery_errors = false 53 | 54 | # Enable threaded mode 55 | # config.threadsafe! 56 | 57 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 58 | # the I18n.default_locale when a translation can not be found) 59 | config.i18n.fallbacks = true 60 | 61 | # Send deprecation notices to registered listeners 62 | config.active_support.deprecation = :notify 63 | 64 | end 65 | -------------------------------------------------------------------------------- /lib/nunes/subscribers/action_controller.rb: -------------------------------------------------------------------------------- 1 | require "nunes/subscriber" 2 | 3 | module Nunes 4 | module Subscribers 5 | class ActionController < ::Nunes::Subscriber 6 | # Private 7 | Pattern = /\.action_controller\Z/ 8 | 9 | # Private: The namespace for events to subscribe to. 10 | def self.pattern 11 | Pattern 12 | end 13 | 14 | def process_action(start, ending, transaction_id, payload) 15 | controller = payload[:controller] 16 | action = payload[:action] 17 | status = payload[:status] 18 | exception_info = payload[:exception] 19 | 20 | format = payload[:format] || "all" 21 | format = "all" if format == "*/*" 22 | 23 | db_runtime = payload[:db_runtime] 24 | db_runtime = db_runtime.round if db_runtime 25 | 26 | view_runtime = payload[:view_runtime] 27 | view_runtime = view_runtime.round if view_runtime 28 | 29 | runtime = ((ending - start) * 1_000).round 30 | 31 | timing "action_controller.runtime.total", runtime 32 | timing "action_controller.runtime.view", view_runtime if view_runtime 33 | timing "action_controller.runtime.db", db_runtime if db_runtime 34 | 35 | increment "action_controller.format.#{format}" if format 36 | increment "action_controller.status.#{status}" if status 37 | 38 | if controller && action 39 | namespace = "action_controller.controller.#{controller}.#{action}" 40 | 41 | timing "#{namespace}.runtime.total", runtime 42 | timing "#{namespace}.runtime.view", view_runtime if view_runtime 43 | timing "#{namespace}.runtime.db", db_runtime if db_runtime 44 | 45 | increment "#{namespace}.format.#{format}" if format 46 | increment "#{namespace}.status.#{status}" if status 47 | 48 | end 49 | 50 | if exception_info 51 | exception_class, exception_message = exception_info 52 | 53 | increment "action_controller.exception.#{exception_class}" 54 | end 55 | end 56 | 57 | ########################################################################## 58 | # All of the events below don't really matter. Most of them also go # 59 | # through process_action. The only value that could be pulled from them # 60 | # would be topk related which graphite doesn't do. # 61 | ########################################################################## 62 | 63 | def start_processing(*) 64 | # noop 65 | end 66 | 67 | def halted_callback(*) 68 | # noop 69 | end 70 | 71 | def redirect_to(*) 72 | # noop 73 | end 74 | 75 | def send_file(*) 76 | # noop 77 | end 78 | 79 | def send_data(*) 80 | # noop 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test/controller_instrumentation_test.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | class ControllerInstrumentationTest < ActionController::TestCase 4 | tests PostsController 5 | 6 | setup :setup_subscriber 7 | teardown :teardown_subscriber 8 | 9 | def setup_subscriber 10 | @subscriber = Nunes::Subscribers::ActionController.subscribe(adapter) 11 | end 12 | 13 | def teardown_subscriber 14 | ActiveSupport::Notifications.unsubscribe @subscriber if @subscriber 15 | end 16 | 17 | test "process_action" do 18 | get :index 19 | 20 | assert_response :success 21 | 22 | assert_counter "action_controller.status.200" 23 | assert_counter "action_controller.format.html" 24 | 25 | assert_timer "action_controller.runtime.total" 26 | assert_timer "action_controller.runtime.view" 27 | 28 | assert_timer "action_controller.controller.PostsController.index.runtime.total" 29 | assert_timer "action_controller.controller.PostsController.index.runtime.view" 30 | end 31 | 32 | test "send_data" do 33 | get :some_data 34 | 35 | assert_response :success 36 | 37 | assert_counter "action_controller.status.200" 38 | 39 | assert_timer "action_controller.runtime.total" 40 | assert_timer "action_controller.runtime.view" 41 | 42 | assert_timer "action_controller.controller.PostsController.some_data.runtime.total" 43 | assert_timer "action_controller.controller.PostsController.some_data.runtime.view" 44 | end 45 | 46 | test "send_file" do 47 | get :some_file 48 | 49 | assert_response :success 50 | 51 | assert_counter"action_controller.status.200" 52 | 53 | assert_timer "action_controller.runtime.total" 54 | assert_timer "action_controller.controller.PostsController.some_file.runtime.total" 55 | 56 | assert ! adapter.timer?("action_controller.runtime.view") 57 | assert ! adapter.timer?("action_controller.controller.PostsController.some_file.runtime.view") 58 | end 59 | 60 | test "redirect_to" do 61 | get :some_redirect 62 | 63 | assert_response :redirect 64 | 65 | assert_counter "action_controller.status.302" 66 | 67 | assert_timer "action_controller.runtime.total" 68 | assert_timer "action_controller.controller.PostsController.some_redirect.runtime.total" 69 | 70 | assert_no_timer "action_controller.runtime.view" 71 | assert_no_timer "action_controller.controller.PostsController.some_redirect.runtime.view" 72 | end 73 | 74 | test "action with exception" do 75 | get :some_boom rescue StandardError # catch the boom 76 | 77 | assert_response :success 78 | 79 | assert_counter "action_controller.exception.RuntimeError" 80 | assert_counter "action_controller.format.html" 81 | 82 | assert_timer "action_controller.runtime.total" 83 | assert_timer "action_controller.controller.PostsController.some_boom.runtime.total" 84 | 85 | assert_no_timer "action_controller.runtime.view" 86 | assert_no_timer "action_controller.controller.PostsController.some_boom.runtime.view" 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /test/rails_app/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | # Pick the frameworks you want: 4 | require "active_record/railtie" 5 | require "action_controller/railtie" 6 | require "action_mailer/railtie" 7 | require "sprockets/railtie" 8 | require "rails/test_unit/railtie" 9 | 10 | if defined?(Bundler) 11 | # If you precompile assets before deploying to production, use this line 12 | Bundler.require(*Rails.groups(:assets => %w(development test))) 13 | # If you want your assets lazily compiled in production, use this line 14 | # Bundler.require(:default, :assets, Rails.env) 15 | end 16 | 17 | module RailsApp 18 | class Application < Rails::Application 19 | # Settings in config/environments/* take precedence over those specified here. 20 | # Application configuration should go into files in config/initializers 21 | # -- all .rb files in that directory are automatically loaded. 22 | 23 | # Custom directories with classes and modules you want to be autoloadable. 24 | # config.autoload_paths += %W(#{config.root}/extras) 25 | 26 | # Only load the plugins named here, in the order given (default is alphabetical). 27 | # :all can be used as a placeholder for all plugins not explicitly named. 28 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 29 | 30 | # Activate observers that should always be running. 31 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 32 | 33 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 34 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 35 | # config.time_zone = 'Central Time (US & Canada)' 36 | 37 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 38 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 39 | # config.i18n.default_locale = :de 40 | 41 | # Configure the default encoding used in templates for Ruby 1.9. 42 | config.encoding = "utf-8" 43 | 44 | # Configure sensitive parameters which will be filtered from the log file. 45 | config.filter_parameters += [:password] 46 | 47 | # Enable escaping HTML in JSON. 48 | config.active_support.escape_html_entities_in_json = true 49 | 50 | # Use SQL instead of Active Record's schema dumper when creating the database. 51 | # This is necessary if your schema can't be completely dumped by the schema dumper, 52 | # like if you have constraints or database-specific column types 53 | # config.active_record.schema_format = :sql 54 | 55 | # Enforce whitelist mode for mass assignment. 56 | # This will create an empty whitelist of attributes available for mass-assignment for all models 57 | # in your app. As such, your models will need to explicitly whitelist or blacklist accessible 58 | # parameters by using an attr_accessible or attr_protected declaration. 59 | # config.active_record.whitelist_attributes = true 60 | 61 | # Enable the asset pipeline 62 | config.assets.enabled = true 63 | 64 | # Version of your assets, change this if you want to expire all your assets 65 | config.assets.version = '1.0' 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /test/adapter_test.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | require "minitest/mock" 3 | 4 | class AdapterTest < ActiveSupport::TestCase 5 | def separator 6 | Nunes::Adapter::Separator 7 | end 8 | 9 | test "wrap for statsd" do 10 | client_with_gauge_and_timing = Class.new do 11 | def increment(*args); end 12 | def gauge(*args); end 13 | def timing(*args); end 14 | end.new 15 | 16 | adapter = Nunes::Adapter.wrap(client_with_gauge_and_timing) 17 | assert_instance_of Nunes::Adapter, adapter 18 | end 19 | 20 | test "wrap for instrumental" do 21 | client_with_gauge_but_not_timing = Class.new do 22 | def increment(*args); end 23 | def gauge(*args); end 24 | end.new 25 | 26 | adapter = Nunes::Adapter.wrap(client_with_gauge_but_not_timing) 27 | assert_instance_of Nunes::Adapters::TimingAliased, adapter 28 | end 29 | 30 | test "wrap for adapter" do 31 | memory = Nunes::Adapters::Memory.new 32 | adapter = Nunes::Adapter.wrap(memory) 33 | assert_equal memory, adapter 34 | end 35 | 36 | test "wrap with hash" do 37 | hash = {} 38 | adapter = Nunes::Adapter.wrap(hash) 39 | assert_instance_of Nunes::Adapters::Memory, adapter 40 | end 41 | 42 | test "wrap with nil" do 43 | assert_raises(ArgumentError) { Nunes::Adapter.wrap(nil) } 44 | end 45 | 46 | test "wrap with straight up gibberish yo" do 47 | assert_raises(ArgumentError) { Nunes::Adapter.wrap(Object.new) } 48 | end 49 | 50 | test "passes increment along" do 51 | mock = MiniTest::Mock.new 52 | mock.expect :increment, nil, ["single", 1] 53 | mock.expect :increment, nil, ["double", 2] 54 | 55 | client = Nunes::Adapter.new(mock) 56 | client.increment("single") 57 | client.increment("double", 2) 58 | 59 | mock.verify 60 | end 61 | 62 | test "passes timing along" do 63 | mock = MiniTest::Mock.new 64 | mock.expect :timing, nil, ["foo", 23] 65 | 66 | client = Nunes::Adapter.new(mock) 67 | client.timing("foo", 23) 68 | 69 | mock.verify 70 | end 71 | 72 | test "prepare leaves good metrics alone" do 73 | adapter = Nunes::Adapter.new(nil) 74 | 75 | [ 76 | "foo", 77 | "foo1234", 78 | "foo-bar", 79 | "foo_bar", 80 | "Foo", 81 | "FooBar", 82 | "FOOBAR", 83 | "foo#{separator}bar", 84 | "foo#{separator}bar_baz", 85 | "foo#{separator}bar_baz-wick", 86 | "Foo#{separator}1234", 87 | "Foo#{separator}Bar1234", 88 | ].each do |expected| 89 | assert_equal expected, adapter.prepare(expected) 90 | end 91 | end 92 | 93 | test "prepare with bad metric names" do 94 | adapter = Nunes::Adapter.new(nil) 95 | 96 | { 97 | "#{separator}foo" => "foo", 98 | "foo#{separator}" => "foo", 99 | "foo@bar" => "foo#{separator}bar", 100 | "foo@$%^*^&bar" => "foo#{separator}bar", 101 | "foo#{separator}#{separator}bar" => "foo#{separator}bar", 102 | "app/views/posts" => "app#{separator}views#{separator}posts", 103 | "Admin::PostsController#{separator}index" => "Admin#{separator}PostsController#{separator}index", 104 | }.each do |metric, expected| 105 | assert_equal expected, adapter.prepare(metric) 106 | end 107 | end 108 | 109 | test "prepare does not modify original metric object" do 110 | adapter = Nunes::Adapter.new(nil) 111 | original = "app.views.posts" 112 | result = adapter.prepare("original") 113 | 114 | assert_equal "app.views.posts", original 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /test/instrumentable_test.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | class InstrumentationTest < ActiveSupport::TestCase 4 | attr_reader :thing_class 5 | 6 | setup :setup_subscriber, :setup_class 7 | teardown :teardown_subscriber, :teardown_class 8 | 9 | def setup_subscriber 10 | @subscriber = Nunes::Subscribers::Nunes.subscribe(adapter) 11 | end 12 | 13 | def teardown_subscriber 14 | ActiveSupport::Notifications.unsubscribe @subscriber if @subscriber 15 | end 16 | 17 | def setup_class 18 | @thing_class = Class.new { 19 | extend Nunes::Instrumentable 20 | 21 | class << self 22 | extend Nunes::Instrumentable 23 | 24 | def find(*args) 25 | :nope 26 | end 27 | end 28 | 29 | def self.name 30 | 'Thing' 31 | end 32 | 33 | def yo(args = {}) 34 | :dude 35 | end 36 | } 37 | end 38 | 39 | def teardown_class 40 | @thing_class = nil 41 | end 42 | 43 | test "adds methods when extended" do 44 | assert thing_class.respond_to?(:instrument_method_time) 45 | end 46 | 47 | test "attempting to instrument time for method twice" do 48 | thing_class.instrument_method_time :yo 49 | 50 | assert_raises(ArgumentError, "already instrumented yo for Thing") do 51 | thing_class.instrument_method_time :yo 52 | end 53 | end 54 | 55 | test "instrument_method_time" do 56 | thing_class.instrument_method_time :yo 57 | 58 | event = slurp_events { thing_class.new.yo(some: 'thing') }.last 59 | 60 | assert_not_nil event, "No events were found." 61 | assert_equal "Thing.yo", event.payload[:metric] 62 | assert_in_delta 0, event.duration, 0.1 63 | 64 | assert_timer "Thing.yo" 65 | end 66 | 67 | test "instrument_method_time for class method without full metric name" do 68 | # I'd really like to not do this, but I don't have a name for the class 69 | # which makes it hard to automatically set the name. If anyone has a fix, 70 | # let me know. 71 | assert_raises ArgumentError do 72 | thing_class.singleton_class.instrument_method_time :find 73 | end 74 | end 75 | 76 | test "instrument_method_time for class method" do 77 | thing_class.singleton_class.instrument_method_time :find, "Thing.find" 78 | 79 | event = slurp_events { thing_class.find(1) }.last 80 | 81 | assert_not_nil event, "No events were found." 82 | assert_equal "Thing.find", event.payload[:metric] 83 | assert_in_delta 0, event.duration, 0.1 84 | 85 | assert_timer "Thing.find" 86 | end 87 | 88 | test "instrument_method_time with custom name in hash" do 89 | thing_class.instrument_method_time :yo, name: 'Thingy.yohoho' 90 | 91 | event = slurp_events { thing_class.new.yo(some: 'thing') }.last 92 | 93 | assert_not_nil event, "No events were found." 94 | assert_equal "Thingy.yohoho", event.payload[:metric] 95 | 96 | assert_timer "Thingy.yohoho" 97 | end 98 | 99 | test "instrument_method_time with custom name as string" do 100 | thing_class.instrument_method_time :yo, 'Thingy.yohoho' 101 | 102 | event = slurp_events { thing_class.new.yo(some: 'thing') }.last 103 | 104 | assert_not_nil event, "No events were found." 105 | assert_equal "Thingy.yohoho", event.payload[:metric] 106 | 107 | assert_timer "Thingy.yohoho" 108 | end 109 | 110 | test "instrument_method_time with custom payload" do 111 | thing_class.instrument_method_time :yo, payload: {pay: "loadin"} 112 | 113 | event = slurp_events { thing_class.new.yo(some: 'thing') }.last 114 | 115 | assert_not_nil event, "No events were found." 116 | assert_equal "loadin", event.payload[:pay] 117 | 118 | assert_timer "Thing.yo" 119 | end 120 | 121 | def slurp_events(&block) 122 | events = [] 123 | callback = lambda { |*args| events << ActiveSupport::Notifications::Event.new(*args) } 124 | ActiveSupport::Notifications.subscribed(callback, Nunes::Instrumentable::MethodTimeEventName, &block) 125 | events 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nunes 2 | 3 | The friendly gem that instruments everything for you, like I would if I could. 4 | 5 | ## Why "nunes"? 6 | 7 | Because I don't work for you, but even that could not stop me from trying to make it as easy as possible for you to instrument ALL THE THINGS. 8 | 9 | ## Installation 10 | 11 | Add this line to your application's Gemfile: 12 | 13 | gem "nunes" 14 | 15 | Or install it yourself as: 16 | 17 | $ gem install nunes 18 | 19 | ## Compatibility 20 | 21 | * >= Ruby 1.9 22 | * Rails 4.2.x 23 | 24 | Note: you can use v0.3.1 is for rails 3.2.x support. 25 | 26 | ## Usage 27 | 28 | nunes works out of the box with [instrumental app](http://instrumentalapp.com) (my personal favorite) and [statsd](https://github.com/reinh/statsd). All you need to do is subscribe using an instance of statsd or instrumental's agent and you are good to go. 29 | 30 | ### With Instrumental 31 | 32 | ```ruby 33 | require "nunes" 34 | I = Instrument::Agent.new(...) 35 | Nunes.subscribe(I) 36 | ``` 37 | 38 | ### With Statsd 39 | 40 | ```ruby 41 | require "nunes" 42 | statsd = Statsd.new(...) 43 | Nunes.subscribe(statsd) 44 | ``` 45 | 46 | ### With Some Other Service 47 | 48 | If you would like for nunes to work with some other service, you can easily make an adapter. Check out the [existing adapters](https://github.com/jnunemaker/nunes/tree/master/lib/nunes/adapters) for examples. The key is to inherit from `Nunes::Adapter` and then convert the `increment` and `timing` methods to whatever the service requires. 49 | 50 | ## What Can I Do For You? 51 | 52 | If you are using nunes with Rails, I will subscribe to the following events: 53 | 54 | * `process_action.action_controller` 55 | * `render_template.action_view` 56 | * `render_partial.action_view` 57 | * `deliver.action_mailer` 58 | * `receive.action_mailer` 59 | * `sql.active_record` 60 | * `cache_read.active_support` 61 | * `cache_generate.active_support` 62 | * `cache_fetch_hit.active_support` 63 | * `cache_write.active_support` 64 | * `cache_delete.active_support` 65 | * `cache_exist?.active_support` 66 | 67 | Whoa! You would do all that for me? Yep, I would. Because I care. Deeply. 68 | 69 | Based on those events, you'll get metrics like this in instrumental and statsd: 70 | 71 | #### Counters 72 | 73 | * `action_controller.status.200` 74 | * `action_controller.format.html` 75 | * `action_controller.controller.Admin.PostsController.new.status.403` 76 | * `action_controller.controller.Admin.PostsController.index.format.json` 77 | * `action_controller.exception.RuntimeError` - where RuntimeError is the class of any exceptions that occur while processing a controller's action. 78 | * `active_support.cache.hit` 79 | * `active_support.cache.miss` 80 | 81 | #### Timers 82 | 83 | * `action_controller.runtime.total` 84 | * `action_controller.runtime.view` 85 | * `action_controller.runtime.db` 86 | * `action_controller.controller.PostsController.index.runtime.total` 87 | * `action_controller.controller.PostsController.index.runtime.view` 88 | * `action_controller.controller.PostsController.index.runtime.db` 89 | * `action_controller.controller.PostsController.index.status.200` 90 | * `action_controller.controller.PostsController.index.format.html` 91 | * `action_view.template.app.views.posts.index.html.erb` - where `app.views.posts.index.html.erb` is the path of the view file 92 | * `action_view.partial.app.views.posts._post.html.erb` - I can even do partials! woot woot! 93 | * `action_mailer.deliver.PostMailer` 94 | * `action_mailer.receive.PostMailer` 95 | * `active_record.sql` 96 | * `active_record.sql.select` 97 | * `active_record.sql.insert` 98 | * `active_record.sql.update` 99 | * `active_record.sql.delete` 100 | * `active_support.cache.read` 101 | * `active_support.cache.fetch` 102 | * `active_support.cache.fetch_hit` 103 | * `active_support.cache.fetch_generate` 104 | * `active_support.cache.write` 105 | * `active_support.cache.delete` 106 | * `active_support.cache.exist` 107 | 108 | ### But wait, there's more!!! 109 | 110 | In addition to doing all that automagical work for you, I also allow you to wrap your own code with instrumentation. I know, I know, sounds too good to be true. 111 | 112 | ```ruby 113 | class User < ActiveRecord::Base 114 | extend Nunes::Instrumentable 115 | 116 | # wrap save and instrument the timing of it 117 | instrument_method_time :save 118 | end 119 | ``` 120 | 121 | This will instrument the timing of the User instance method save. What that means is when you do this: 122 | 123 | ```ruby 124 | user = User.new(name: 'NUNES!') 125 | user.save 126 | ``` 127 | 128 | An event named `instrument_method_time.nunes` will be generated, which in turn is subscribed to and sent to whatever you used to send instrumentation to (statsd, instrumental, etc.). The metric name will default to class.method. For the example above, the metric name would be `User.save`. No fear, you can customize this. 129 | 130 | ```ruby 131 | class User < ActiveRecord::Base 132 | extend Nunes::Instrumentable 133 | 134 | # wrap save and instrument the timing of it 135 | instrument_method_time :save, 'crazy_town.save' 136 | end 137 | ``` 138 | 139 | Passing a string as the second argument sets the name of the metric. You can also customize the name using a Hash as the second argument. 140 | 141 | ```ruby 142 | class User < ActiveRecord::Base 143 | extend Nunes::Instrumentable 144 | 145 | # wrap save and instrument the timing of it 146 | instrument_method_time :save, name: 'crazy_town.save' 147 | end 148 | ``` 149 | 150 | In addition to name, you can also pass a payload that will get sent along with the generated event. 151 | 152 | ```ruby 153 | class User < ActiveRecord::Base 154 | extend Nunes::Instrumentable 155 | 156 | # wrap save and instrument the timing of it 157 | instrument_method_time :save, payload: {pay: "loading"} 158 | end 159 | ``` 160 | 161 | If you subscribe to the event on your own, say to log some things, you'll get a key named `:pay` with a value of `"loading"` in the event's payload. Pretty neat, eh? 162 | 163 | ## `script/bootstrap` 164 | 165 | This script will get all the dependencies ready so you can start hacking on nunes. 166 | 167 | ``` 168 | # to learn more about script/bootstrap 169 | script/bootstrap help 170 | ``` 171 | 172 | ## `script/test` 173 | 174 | For your convenience, there is a script to run the tests. It will also perform `script/bootstrap`, which bundles and all that jazz. 175 | 176 | ``` 177 | # to learn more about script test 178 | script/test help 179 | ``` 180 | 181 | ## `script/watch` 182 | 183 | If you are like me, you are too lazy to continually run `script/test`. For this scenario, I have included `script/watch`, which will run `script/test` automatically anytime a relevant file changes. 184 | 185 | ## Contributing 186 | 187 | 1. Fork it 188 | 2. Create your feature branch (`git checkout -b my-new-feature`) 189 | 3. Commit your changes (`git commit -am 'Add some feature'`) 190 | 4. Push to the branch (`git push origin my-new-feature`) 191 | 5. Create new Pull Request 192 | -------------------------------------------------------------------------------- /test/rails_app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Ruby on Rails: Welcome aboard 5 | 174 | 187 | 188 | 189 |
190 | 203 | 204 |
205 | 209 | 210 | 214 | 215 |
216 |

Getting started

217 |

Here’s how to get rolling:

218 | 219 |
    220 |
  1. 221 |

    Use rails generate to create your models and controllers

    222 |

    To see all available options, run it without parameters.

    223 |
  2. 224 | 225 |
  3. 226 |

    Set up a default route and remove public/index.html

    227 |

    Routes are set up in config/routes.rb.

    228 |
  4. 229 | 230 |
  5. 231 |

    Create your database

    232 |

    Run rake db:create to create your database. If you're not using SQLite (the default), edit config/database.yml with your username and password.

    233 |
  6. 234 |
235 |
236 |
237 | 238 | 239 |
240 | 241 | 242 | --------------------------------------------------------------------------------