├── .gitignore ├── .rspec ├── .travis.yml ├── Appraisals ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── gemfiles ├── as_5.0.gemfile ├── as_5.1.gemfile ├── as_5.2.gemfile └── as_6.0.gemfile ├── lib ├── rails-callback_log.rb └── rails_callback_log │ └── version.rb ├── rails-callback_log.gemspec └── spec ├── output_spec.rb ├── rails-callback_log_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | .ruby-version 11 | gemfiles/.bundle 12 | gemfiles/*.gemfile.lock 13 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | gemfile: 2 | - gemfiles/as_5.0.gemfile 3 | - gemfiles/as_5.1.gemfile 4 | - gemfiles/as_5.2.gemfile 5 | - gemfiles/as_6.0.gemfile 6 | language: ruby 7 | matrix: 8 | fast_finish: true 9 | exclude: 10 | - gemfile: gemfiles/as_6.0.gemfile 11 | rvm: 2.4 12 | rvm: 13 | - 2.4 14 | - 2.5 15 | - 2.6 16 | sudo: false 17 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | # Specify here only version constraints that differ from 2 | # `paper_trail.gemspec`. 3 | # 4 | # > The dependencies in your Appraisals file are combined with dependencies in 5 | # > your Gemfile, so you don't need to repeat anything that's the same for each 6 | # > appraisal. If something is specified in both the Gemfile and an appraisal, 7 | # > the version from the appraisal takes precedence. 8 | # > https://github.com/thoughtbot/appraisal 9 | 10 | appraise "as-5.0" do 11 | gem "activesupport", "~> 5.0.0" 12 | end 13 | 14 | appraise "as-5.1" do 15 | gem "activesupport", "~> 5.1.0" 16 | end 17 | 18 | appraise "as-5.2" do 19 | gem "activesupport", "~> 5.2.0" 20 | end 21 | 22 | appraise "as-6.0" do 23 | gem "activesupport", "~> 6.0.0" 24 | end 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This gem conforms to [semver 2.0.0][1] and follows the recommendations of 4 | [keepachangelog.com][2]. 5 | 6 | ### Unreleased 7 | 8 | Breaking Changes: 9 | 10 | - None 11 | 12 | Added: 13 | 14 | - None 15 | 16 | Fixed: 17 | 18 | - None 19 | 20 | ### 0.3.1 (2019-12-11) 21 | 22 | Breaking Changes: 23 | 24 | - None 25 | 26 | Added: 27 | 28 | - Relax dependency constraint on activesupport, removing upper bound 29 | 30 | Fixed: 31 | 32 | - None 33 | 34 | ### 0.3.0 (2019-01-29) 35 | 36 | Breaking Changes: 37 | 38 | - Drop `RailsCallbackLog::VERSION`, use `RailsCallbackLog.gem_version` 39 | 40 | Added: 41 | 42 | - Support rails 5.2 43 | 44 | Fixed: 45 | 46 | - [#5](https://github.com/jaredbeck/rails-callback_log/pull/5) 47 | Huge improvements to output in rails > 5.1 48 | 49 | ### 0.2.2 (2017-05-08) 50 | 51 | Breaking Changes: 52 | 53 | - None 54 | 55 | Added: 56 | 57 | - None 58 | 59 | Fixed: 60 | 61 | - [#3](https://github.com/jaredbeck/rails-callback_log/pull/3) 62 | Fallback to ::Logger if Rails::Logger not loaded 63 | 64 | ### 0.2.1 (2017-05-02) 65 | 66 | Breaking Changes: 67 | 68 | - None 69 | 70 | Added: 71 | 72 | - None 73 | 74 | Fixed: 75 | 76 | - Fix a performance issue introduced in 0.2.0 77 | 78 | ### 0.2.0 (2017-05-01) 79 | 80 | Breaking Changes: 81 | 82 | - None 83 | 84 | Added: 85 | 86 | - Support for rails 5.1 87 | 88 | ### 0.1.0 (2016-07-25) 89 | 90 | Breaking changes: 91 | 92 | - Drop support for ruby 1.9.3 93 | 94 | Added: 95 | 96 | - Support for rails 5.0 97 | 98 | ### 0.0.3 (2016-06-24) 99 | 100 | Initial release, support for rails 4.2 only. 101 | 102 | [1]: http://semver.org/ 103 | [2]: http://keepachangelog.com/ 104 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This code of conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at jared@jaredbeck.com. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 45 | version 1.3.0, available at 46 | [http://contributor-covenant.org/version/1/3/0/][version] 47 | 48 | [homepage]: http://contributor-covenant.org 49 | [version]: http://contributor-covenant.org/version/1/3/0/ -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jared Beck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RailsCallbackLog 2 | 3 | Do you have a rails app with a lot of callbacks? Are they kind of a mystery? Maybe logging them would help. 4 | 5 | ``` 6 | Started GET "/" for 127.0.0.1 at 2016-07-26 13:25:32 -0400 7 | Processing by HomeController#index as HTML 8 | Callback: verify_authenticity_token 9 | Callback: activate_authlogic 10 | Callback: require_client_subdomain 11 | Client Load (0.4ms) SELECT `clients`.* ... 12 | Callback: check_hostname 13 | Callback: update_last_request_at 14 | ... 15 | ``` 16 | 17 | ## Installation 18 | 19 | ```ruby 20 | # Gemfile 21 | gem "rails-callback_log", group: [:development, :test] 22 | ``` 23 | 24 | Do not use this gem in production because it adds significant overhead. 25 | 26 | ## Filtering Output 27 | 28 | Rails has a lot of its own callbacks that you probably don't care about. If you 29 | don't want to log them, enable filtering. 30 | 31 | ```bash 32 | # Enable filtering 33 | export RAILS_CALLBACK_LOG_FILTER="make it so" 34 | 35 | # Disable filtering 36 | unset RAILS_CALLBACK_LOG_FILTER 37 | ``` 38 | 39 | Filtering incurs a serious performance penalty, so it is off by default. 40 | 41 | ## Team Opt-in 42 | 43 | When working in a team and logging is needed only for select members, use this 44 | environment-based opt-in pattern: 45 | 46 | ```ruby 47 | # Gemfile 48 | gem "rails-callback_log", 49 | group: [:development, :test], 50 | require: (ENV["LOG_RAILS_CALLBACKS"] == "true") 51 | ``` 52 | 53 | ## See Also 54 | 55 | - http://stackoverflow.com/q/13089936/567762 56 | 57 | ## License 58 | 59 | The gem is available as open source under the terms of the [MIT 60 | License](http://opensource.org/licenses/MIT). 61 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /gemfiles/as_5.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activesupport", "~> 5.0.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/as_5.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activesupport", "~> 5.1.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/as_5.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activesupport", "~> 5.2.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/as_6.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activesupport", "~> 6.0.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /lib/rails-callback_log.rb: -------------------------------------------------------------------------------- 1 | require "rails_callback_log/version" 2 | 3 | # Make sure `ActiveSupport::Callbacks` is loaded before we continue. 4 | require "active_support/all" 5 | 6 | module RailsCallbackLog 7 | # Filtering is very expensive. It makes my test suite more than 50% 8 | # slower. So, it's off by default. 9 | FILTER = ENV["RAILS_CALLBACK_LOG_FILTER"].present?.freeze 10 | 11 | class << self 12 | def logger 13 | ::Rails.logger || ::Logger.new(STDOUT) 14 | end 15 | 16 | def matches_filter?(str) 17 | source_location_filters.any? { |f| str.start_with?(f) } 18 | end 19 | 20 | def log(msg) 21 | if !FILTER || caller.any? { |line| matches_filter?(line) } 22 | logger.debug(format("Callback: %s", msg)) 23 | end 24 | end 25 | 26 | private 27 | 28 | def source_location_filters 29 | @@filters ||= %w(app lib).map { |dir| (::Rails.root + dir).to_s } 30 | end 31 | end 32 | 33 | # In rails 5.1, we extend `CallTemplate`. 34 | module CallTemplateExtension 35 | # Rails 5.1 and above call `expand` to get a reference to the object and 36 | # method of the callback to execute. If the callback was a proc then 37 | # @override_block will be set. If the callback was an object then 38 | # @override_target will be set. If the callback was a symbol method name 39 | # then @method_name will be set. 40 | def expand(target, value, block) 41 | ::RailsCallbackLog.log(@override_block || @override_target || @method_name) 42 | 43 | super(target, value, block) 44 | end 45 | end 46 | 47 | # In rails 4.2 and 5.0, we extend `Callback`. 48 | module CallbackExtension 49 | # Returns a lambda that wraps `super`, adding logging. 50 | def make_lambda(filter) 51 | original_lambda = super(filter) 52 | lambda { |*args, &block| 53 | ::RailsCallbackLog.log(filter) 54 | 55 | original_lambda.call(*args, &block) 56 | } 57 | end 58 | end 59 | end 60 | 61 | # Install our `CallbackExtension` using module prepend. 62 | module ActiveSupport 63 | module Callbacks 64 | if ::ActiveSupport.gem_version >= ::Gem::Version.new("5.1.0") 65 | class CallTemplate 66 | prepend ::RailsCallbackLog::CallTemplateExtension 67 | end 68 | else 69 | class Callback 70 | prepend ::RailsCallbackLog::CallbackExtension 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/rails_callback_log/version.rb: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | 3 | module RailsCallbackLog 4 | def self.gem_version 5 | ::Gem::Version.new('0.3.1') 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /rails-callback_log.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "rails_callback_log/version" 5 | 6 | ::Gem::Specification.new do |spec| 7 | spec.name = "rails-callback_log" 8 | spec.version = ::RailsCallbackLog.gem_version.to_s 9 | spec.authors = ["Jared Beck"] 10 | spec.email = ["jared@jaredbeck.com"] 11 | spec.summary = "Logs callbacks to help with debugging." 12 | spec.homepage = "https://github.com/jaredbeck/rails-callback_log" 13 | spec.license = "MIT" 14 | spec.files = [ 15 | "Gemfile", # necessary? 16 | "LICENSE.txt", # because: lawyers 17 | "lib/rails-callback_log.rb", 18 | "lib/rails_callback_log/version.rb", 19 | "rails-callback_log.gemspec" 20 | ] 21 | spec.require_paths = ["lib"] 22 | spec.required_ruby_version = ">= 2.0" 23 | 24 | # No upper bound, but see `Appraisals` for a list of versions that are 25 | # actually tested. 26 | spec.add_runtime_dependency "activesupport", [">= 4.2.0"] 27 | 28 | spec.add_development_dependency "appraisal", "~> 2.2" 29 | spec.add_development_dependency "bundler", [">= 1.12", "< 3"] 30 | spec.add_development_dependency "rake", "~> 12.0" 31 | spec.add_development_dependency "rspec", "~> 3.0" 32 | end 33 | -------------------------------------------------------------------------------- /spec/output_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | class CallbackWrapper 4 | def after(record) 5 | Rails.logger.info("- in CallbackWrapper after method") 6 | end 7 | end 8 | 9 | class CallbackTester 10 | include ActiveSupport::Callbacks 11 | define_callbacks :save 12 | 13 | def save 14 | run_callbacks :save do 15 | Rails.logger.info("- save") 16 | end 17 | end 18 | 19 | # Set a proc based callback 20 | set_callback :save, :before do 21 | Rails.logger.info("- in before save callback") 22 | end 23 | 24 | # Set an object based callback, will call `after` on CallbackWrapper 25 | set_callback :save, :after, CallbackWrapper.new 26 | 27 | # Set a symbol based method reference callback 28 | set_callback :save, :around, :around_callback 29 | def around_callback 30 | Rails.logger.info("- in around callback before") 31 | yield 32 | Rails.logger.info("- in around callback after") 33 | end 34 | end 35 | 36 | describe RailsCallbackLog do 37 | # Filter the logs down to only the lines that start with Callback. Makes the 38 | # test output easier to read. 39 | subject(:filtered_log) do 40 | output.string.split("\n").select { |line| line =~ /^Callback/ }.join("\n") 41 | end 42 | 43 | # Create a logger that logs to a StringIO that we can test against 44 | let(:output) { StringIO.new } 45 | let(:logger) do 46 | logger = Logger.new(output) 47 | logger.formatter = proc { |severity, datetime, progname, msg| 48 | "#{msg}\n" 49 | } 50 | logger 51 | end 52 | 53 | before do 54 | # Set the logger to our custom StringIO logger 55 | Rails.logger = logger 56 | end 57 | 58 | describe ".log" do 59 | it "logs all of the callbacks" do 60 | CallbackTester.new.save 61 | 62 | expect(filtered_log).to include "Callback: #= ::Gem::Version.new("5.1.0") 6 | RSpec.describe CallTemplate do 7 | describe "#make_lambda" do 8 | it "returns a Proc that calls the rails logger" do 9 | callback = double("mock callback") 10 | cb = described_class.build(:banana, callback) 11 | 12 | # Expect `make_lambda` to return a `Proc`. 13 | lambda_with_log = cb.make_lambda 14 | expect(lambda_with_log).to be_a(::Proc) 15 | 16 | # Mock the rails logger. We're going to expect it to receive a `debug` message. 17 | logger = double 18 | allow(logger).to receive(:debug) 19 | allow(::RailsCallbackLog).to receive(:logger).and_return(logger) 20 | target = double 21 | 22 | # Symbol-based callbacks have a `target` and a `value`. The definition of 23 | # these arguments is not clear to me yet. My guess is an AR callback's `target` 24 | # would be a model instance, for example. 25 | allow(target).to receive(:kiwi).with(:banana) 26 | allow(target).to receive(:banana) 27 | 28 | # Call our lambda and expect it to send a debug message to our mock logger. 29 | lambda_with_log.call(target, :kiwi) 30 | expect(logger).to have_received(:debug).once.with("Callback: banana") 31 | expect(target).to have_received(:banana).once 32 | end 33 | end 34 | end 35 | else 36 | RSpec.describe Callback do 37 | describe "#make_lambda" do 38 | it "returns a Proc that calls the rails logger" do 39 | # Instantiate a `Callback`. For this test, none of the arguments matter. 40 | name = double.as_null_object 41 | filter = double.as_null_object 42 | kind = double.as_null_object 43 | options = double.as_null_object 44 | chain_config = double.as_null_object 45 | cb = described_class.new(name, filter, kind, options, chain_config) 46 | 47 | # Expect `make_lambda` to return a `Proc`. 48 | lambda_with_log = cb.make_lambda(:banana) 49 | expect(lambda_with_log).to be_a(::Proc) 50 | 51 | # Mock the rails logger. We're going to expect it to receive a `debug` message. 52 | logger = double 53 | allow(logger).to receive(:debug) 54 | allow(::Rails).to receive(:logger).and_return(logger) 55 | target = double 56 | 57 | # Symbol-based callbacks have a `target` and a `value`. The definition of 58 | # these arguments is not clear to me yet. My guess is an AR callback's `target` 59 | # would be a model instance, for example. 60 | allow(target).to receive(:kiwi).with(:banana) 61 | allow(target).to receive(:banana) 62 | 63 | # Call our lambda and expect it to send a debug message to our mock logger. 64 | lambda_with_log.call(target, :kiwi) 65 | expect(logger).to have_received(:debug).once.with("Callback: banana") 66 | expect(target).to have_received(:banana).once 67 | end 68 | end 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | 3 | # When we load `rails-callback_log.rb`, it will want to know what version of rails we 4 | # is loaded. We don't want to load all of rails just to run our tests, so we simply 5 | # define `::Rails.gem_version`. 6 | module Rails 7 | class << self 8 | attr_accessor :logger 9 | end 10 | 11 | def self.gem_version 12 | ::ActiveSupport.gem_version 13 | end 14 | end 15 | 16 | require "rails-callback_log" 17 | --------------------------------------------------------------------------------