marginalia-1.11.1/ 0000755 0000041 0000041 00000000000 14107734070 013732 5 ustar www-data www-data marginalia-1.11.1/test/ 0000755 0000041 0000041 00000000000 14107734070 014711 5 ustar www-data www-data marginalia-1.11.1/test/query_comments_test.rb 0000644 0000041 0000041 00000026626 14107734070 021363 0 ustar www-data www-data # -*- coding: utf-8 -*- require 'rails/version' def using_rails_api? ENV["TEST_RAILS_API"] == true end def pool_db_config? Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new('6.1') end require "minitest/autorun" require "mocha/minitest" require 'logger' require 'pp' require 'active_record' require 'action_controller' require 'active_job' require 'sidekiq' require 'sidekiq/testing' require 'action_dispatch/middleware/request_id' if using_rails_api? require 'rails-api/action_controller/api' end # Shim for compatibility with older versions of MiniTest MiniTest::Test = MiniTest::Unit::TestCase unless defined?(MiniTest::Test) # From version 4.1, ActiveRecord expects `Rails.env` to be # defined if `Rails` is defined if defined?(Rails) && !defined?(Rails.env) module Rails def self.env end end end require 'marginalia' RAILS_ROOT = File.expand_path(File.dirname(__FILE__)) ActiveRecord::Base.establish_connection({ :adapter => ENV["DRIVER"] || "mysql", :host => ENV["DB_HOST"] || "localhost", :username => ENV["DB_USERNAME"] || "root", :database => "marginalia_test" }) class Post < ActiveRecord::Base end class PostsController < ActionController::Base def driver_only ActiveRecord::Base.connection.execute "select id from posts" if Gem::Version.new(Rails::VERSION::STRING) >= Gem::Version.new('5') render body: nil else render nothing: true end end end module API module V1 class PostsController < ::PostsController end end end class PostsJob < ActiveJob::Base def perform Post.first end end class PostsSidekiqJob include Sidekiq::Worker def perform Post.first end end if using_rails_api? class PostsApiController < ActionController::API def driver_only ActiveRecord::Base.connection.execute "select id from posts" head :no_content end end end unless Post.table_exists? ActiveRecord::Schema.define do create_table "posts", :force => true do |t| end end end Marginalia::Railtie.insert class MarginaliaTest < MiniTest::Test def setup # Touch the model to avoid spurious schema queries Post.first @queries = [] ActiveSupport::Notifications.subscribe "sql.active_record" do |*args| @queries << args.last[:sql] end @env = Rack::MockRequest.env_for('/') ActiveJob::Base.queue_adapter = :inline end def test_double_annotate ActiveRecord::Base.connection.expects(:annotate_sql).returns("select id from posts").once ActiveRecord::Base.connection.send(:select, "select id from posts") ensure ActiveRecord::Base.connection.unstub(:annotate_sql) end def test_exists Post.exists? assert_match %r{/\*application:rails\*/$}, @queries.last end def test_query_commenting_on_mysql_driver_with_no_action ActiveRecord::Base.connection.execute "select id from posts" assert_match %r{select id from posts /\*application:rails\*/$}, @queries.first end if ENV["DRIVER"] =~ /^mysql/ def test_query_commenting_on_mysql_driver_with_binary_chars ActiveRecord::Base.connection.execute "select id from posts /* \x81\x80\u0010\ */" assert_equal "select id from posts /* \x81\x80\u0010 */ /*application:rails*/", @queries.first end end if ENV["DRIVER"] =~ /^postgres/ def test_query_commenting_on_postgres_update ActiveRecord::Base.connection.expects(:annotate_sql).returns("update posts set id = 1").once ActiveRecord::Base.connection.send(:exec_update, "update posts set id = 1") ensure ActiveRecord::Base.connection.unstub(:annotate_sql) end def test_query_commenting_on_postgres_delete ActiveRecord::Base.connection.expects(:annotate_sql).returns("delete from posts where id = 1").once ActiveRecord::Base.connection.send(:exec_delete, "delete from posts where id = 1") ensure ActiveRecord::Base.connection.unstub(:annotate_sql) end end def test_query_commenting_on_mysql_driver_with_action PostsController.action(:driver_only).call(@env) assert_match %r{select id from posts /\*application:rails,controller:posts,action:driver_only\*/$}, @queries.first if using_rails_api? PostsApiController.action(:driver_only).call(@env) assert_match %r{select id from posts /\*application:rails,controller:posts_api,action:driver_only\*/$}, @queries.second end end def test_configuring_application Marginalia.application_name = "customapp" PostsController.action(:driver_only).call(@env) assert_match %r{/\*application:customapp,controller:posts,action:driver_only\*/$}, @queries.first if using_rails_api? PostsApiController.action(:driver_only).call(@env) assert_match %r{/\*application:customapp,controller:posts_api,action:driver_only\*/$}, @queries.second end end def test_configuring_query_components Marginalia::Comment.components = [:controller] PostsController.action(:driver_only).call(@env) assert_match %r{/\*controller:posts\*/$}, @queries.first if using_rails_api? PostsApiController.action(:driver_only).call(@env) assert_match %r{/\*controller:posts_api\*/$}, @queries.second end end def test_last_line_component Marginalia::Comment.components = [:line] PostsController.action(:driver_only).call(@env) # Because "lines_to_ignore" by default includes "marginalia" and "gem", the # extracted line line will be from the line in this file that actually # triggers the query. assert_match %r{/\*line:test/query_comments_test.rb:[0-9]+:in `driver_only'\*/$}, @queries.first end def test_last_line_component_with_lines_to_ignore Marginalia::Comment.lines_to_ignore = /foo bar/ Marginalia::Comment.components = [:line] PostsController.action(:driver_only).call(@env) # Because "lines_to_ignore" does not include "marginalia", the extracted # line will be from marginalia/comment.rb. assert_match %r{/\*line:.*lib/marginalia/comment.rb:[0-9]+}, @queries.first end def test_default_lines_to_ignore_regex line = "/gems/a_gem/lib/a_gem.rb:1:in `some_method'" call_stack = [line] + caller assert_match( call_stack.detect { |line| line !~ Marginalia::Comment::DEFAULT_LINES_TO_IGNORE_REGEX }, line ) end def test_hostname_and_pid Marginalia::Comment.components = [:hostname, :pid] PostsController.action(:driver_only).call(@env) assert_match %r{/\*hostname:#{Socket.gethostname},pid:#{Process.pid}\*/$}, @queries.first end def test_controller_with_namespace Marginalia::Comment.components = [:controller_with_namespace] API::V1::PostsController.action(:driver_only).call(@env) assert_match %r{/\*controller_with_namespace:API::V1::PostsController}, @queries.first end def test_db_host Marginalia::Comment.components = [:db_host] API::V1::PostsController.action(:driver_only).call(@env) assert_match %r{/\*db_host:#{ENV["DB_HOST"] || "localhost"}}, @queries.first end def test_database Marginalia::Comment.components = [:database] API::V1::PostsController.action(:driver_only).call(@env) assert_match %r{/\*database:marginalia_test}, @queries.first end if pool_db_config? def test_socket # setting socket in configuration would break some connections - mock it instead pool = ActiveRecord::Base.connection_pool pool.db_config.stubs(:configuration_hash).returns({:socket => "marginalia_socket"}) Marginalia::Comment.components = [:socket] API::V1::PostsController.action(:driver_only).call(@env) assert_match %r{/\*socket:marginalia_socket}, @queries.first pool.db_config.unstub(:configuration_hash) end else def test_socket # setting socket in configuration would break some connections - mock it instead pool = ActiveRecord::Base.connection_pool pool.spec.stubs(:config).returns({:socket => "marginalia_socket"}) Marginalia::Comment.components = [:socket] API::V1::PostsController.action(:driver_only).call(@env) assert_match %r{/\*socket:marginalia_socket}, @queries.first pool.spec.unstub(:config) end end def test_request_id @env["action_dispatch.request_id"] = "some-uuid" Marginalia::Comment.components = [:request_id] PostsController.action(:driver_only).call(@env) assert_match %r{/\*request_id:some-uuid.*}, @queries.first if using_rails_api? PostsApiController.action(:driver_only).call(@env) assert_match %r{/\*request_id:some-uuid.*}, @queries.second end end def test_active_job Marginalia::Comment.components = [:job] PostsJob.perform_later assert_match %{job:PostsJob}, @queries.first Post.first refute_match %{job:PostsJob}, @queries.last end def test_active_job_with_sidekiq Marginalia::Comment.components = [:job, :sidekiq_job] PostsJob.perform_later assert_match %{job:PostsJob}, @queries.first Post.first refute_match %{job:PostsJob}, @queries.last end def test_sidekiq_job Marginalia::Comment.components = [:sidekiq_job] Marginalia::SidekiqInstrumentation.enable! # Test harness does not run Sidekiq middlewares by default so include testing middleware. Sidekiq::Testing.server_middleware do |chain| chain.add Marginalia::SidekiqInstrumentation::Middleware end Sidekiq::Testing.fake! PostsSidekiqJob.perform_async PostsSidekiqJob.drain assert_match %{sidekiq_job:PostsSidekiqJob}, @queries.first Post.first refute_match %{sidekiq_job:PostsSidekiqJob}, @queries.last end def test_good_comment assert_equal Marginalia::Comment.escape_sql_comment('app:foo'), 'app:foo' end def test_bad_comments assert_equal Marginalia::Comment.escape_sql_comment('*/; DROP TABLE USERS;/*'), '; DROP TABLE USERS;' assert_equal Marginalia::Comment.escape_sql_comment('**//; DROP TABLE USERS;/*'), '; DROP TABLE USERS;' end def test_inline_annotations Marginalia.with_annotation("foo") do Post.first end Post.first assert_match %r{/\*foo\*/$}, @queries.first refute_match %r{/\*foo\*/$}, @queries.last # Assert we're not adding an empty comment, either refute_match %r{/\*\s*\*/$}, @queries.last end def test_nested_inline_annotations Marginalia.with_annotation("foo") do Marginalia.with_annotation("bar") do Post.first end end assert_match %r{/\*foobar\*/$}, @queries.first end def test_bad_inline_annotations Marginalia.with_annotation("*/; DROP TABLE USERS;/*") do Post.first end Marginalia.with_annotation("**//; DROP TABLE USERS;//**") do Post.first end assert_match %r{/\*; DROP TABLE USERS;\*/$}, @queries.first assert_match %r{/\*; DROP TABLE USERS;\*/$}, @queries.last end def test_inline_annotations_are_deduped Marginalia.with_annotation("foo") do ActiveRecord::Base.connection.execute "select id from posts /*foo*/" end assert_match %r{select id from posts /\*foo\*/ /\*application:rails\*/$}, @queries.first end def test_add_comments_to_beginning_of_query Marginalia::Comment.prepend_comment = true ActiveRecord::Base.connection.execute "select id from posts" assert_match %r{/\*application:rails\*/ select id from posts$}, @queries.first ensure Marginalia::Comment.prepend_comment = nil end def teardown Marginalia.application_name = nil Marginalia::Comment.lines_to_ignore = nil Marginalia::Comment.components = [:application, :controller, :action] ActiveSupport::Notifications.unsubscribe "sql.active_record" end end marginalia-1.11.1/README.md 0000644 0000041 0000041 00000012570 14107734070 015216 0 ustar www-data www-data # marginalia [](https://github.com/basecamp/marginalia/actions/workflows/ci.yml) Attach comments to your ActiveRecord queries. By default, it adds the application, controller, and action names as a comment at the end of each query. This helps when searching log files for queries, and seeing where slow queries came from. For example, once enabled, your logs will look like: Account Load (0.3ms) SELECT `accounts`.* FROM `accounts` WHERE `accounts`.`queenbee_id` = 1234567890 LIMIT 1 /*application:BCX,controller:project_imports,action:show*/ You can also use these query comments along with a tool like [pt-query-digest](http://www.percona.com/doc/percona-toolkit/2.1/pt-query-digest.html#query-reviews) to automate identification of controllers and actions that are hotspots for slow queries. This gem was created at 37signals. You can read more about how we use it [on our blog](http://37signals.com/svn/posts/3130-tech-note-mysql-query-comments-in-rails). This has been tested and used in production with the mysql2 and pg gems, and is tested on Rails 5.2 through 6.1, and Ruby 2.6 through 3.0. It is also tested for sqlite3. Rails version support will follow supported versions in the [Ruby on Rails maintenance policy](https://guides.rubyonrails.org/maintenance_policy.html) and Ruby support will follow maintained versions in the [Ruby maintenance policy](https://www.ruby-lang.org/en/downloads/branches/). Patches are welcome for other database adapters. ## Installation # Gemfile gem 'marginalia' ### Customization Optionally, you can set the application name shown in the log like so in an initializer (e.g. `config/initializers/marginalia.rb`): Marginalia.application_name = "BCX" The name will default to your Rails application name. #### Components You can also configure the components of the comment that will be appended, by setting `Marginalia::Comment.components`. By default, this is set to: Marginalia::Comment.components = [:application, :controller, :action] Which results in a comment of `application:#{application_name},controller:#{controller.name},action:#{action_name}`. You can re-order or remove these components. You can also add additional comment components of your desire by defining new module methods for `Marginalia::Comment` which return a string. For example: module Marginalia module Comment def self.mycommentcomponent "TEST" end end end Marginalia::Comment.components = [:application, :mycommentcomponent] Which will result in a comment like `application:#{application_name},mycommentcomponent:TEST` The calling controller is available to these methods via `@controller`. Marginalia ships with `:application`, `:controller`, and `:action` enabled by default. In addition, implementation is provided for: * `:line` (for file and line number calling query). :line supports a configuration by setting a regexp in `Marginalia::Comment.lines_to_ignore` to exclude parts of the stacktrace from inclusion in the line comment. * `:controller_with_namespace` to include the full classname (including namespace) of the controller. * `:job` to include the classname of the ActiveJob being performed. * `:hostname` to include ```Socket.gethostname```. * `:pid` to include current process id. * `:db_host` to include the configured database hostname. * `:socket` to include the configured database socket. * `:database` to include the configured database name. Pull requests for other included comment components are welcome. #### Prepend comments By default marginalia appends the comments at the end of the query. Certain databases, such as MySQL will truncate the query text. This is the case for slow query logs and the results of querying some InnoDB internal tables where the length of the query is more than 1024 bytes. In order to not lose the marginalia comments from your logs, you can prepend the comments using this option: Marginalia::Comment.prepend_comment = true #### Inline query annotations In addition to the request or job-level component-based annotations, Marginalia may be used to add inline annotations to specific queries using a block-based API. For example, the following code: Marginalia.with_annotation("foo") do Account.where(queenbee_id: 1234567890).first end will issue this query: Account Load (0.3ms) SELECT `accounts`.* FROM `accounts` WHERE `accounts`.`queenbee_id` = 1234567890 LIMIT 1 /*application:BCX,controller:project_imports,action:show*/ /*foo*/ Nesting `with_annotation` blocks will concatenate the comment strings. ### Caveats #### Prepared statements Be careful when using Marginalia with prepared statements. If you use a component like `request_id` then every query will be unique and so ActiveRecord will create a new prepared statement for each potentially exhausting system resources. [Disable prepared statements](https://guides.rubyonrails.org/configuring.html#configuring-a-postgresql-database) if you wish to use components with high cardinality values. ## Contributing Start by bundling and creating the test database: bundle rake db:mysql:create rake db:postgresql:create Then, running `rake` will run the tests on all the database adapters (`mysql`, `mysql2`, `postgresql` and `sqlite`): rake marginalia-1.11.1/data.tar.gz.sig 0000444 0000041 0000041 00000000400 14107734070 016543 0 ustar www-data www-data yl'G j2X&:^nj|"<>FСA TZO+/@%"%_.pLozc=vLK?1QG!F($w%QC4Xipњ)x!ϩ1xd(`pp?'λt7Z#bc2XMT~#C