├── 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 | -
221 |
Use rails generate to create your models and controllers
222 | To see all available options, run it without parameters.
223 |
224 |
225 | -
226 |
Set up a default route and remove public/index.html
227 | Routes are set up in config/routes.rb.
228 |
229 |
230 | -
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 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
--------------------------------------------------------------------------------