Prosopite Documentation

repository·main·Indexed 23 days ago

https://github.com/charkost/prosopite

A Ruby gem for auto-detecting Rails N+1 queries with high precision. It monitors SQL queries via Active Support instrumentation to identify patterns where multiple queries share the same call stack and fingerprint. Prosopite provides integration for Rails controllers, Rack middleware, Sidekiq middleware (6.5.0+), and test environments, offering configurable reporting via loggers or exceptions.

Tokens
2.5K
Snippets
8
Records
18
Agent score
75%

What's inside Prosopite

  1. Enable N+1 detection in Test Environment

    main

    To make tests fail when N+1 queries are detected, configure the environment and wrap each test in a scan/finish block.

    1. Configure config/environments/test.rb

    config.after_initialize do
      Prosopite.rails_logger = true
      Prosopite.raise = true
    end

    2. Configure spec/spec_helper.rb Note: scan and finish must run around each test, not the whole suite.

    config.before(:each) do
      Prosopite.scan
    end
    
    config.after(:each) do
      Prosopite.finish
    end
    # config/environments/test.rb
    config.after_initialize do
      Prosopite.rails_logger = true
      Prosopite.raise = true
    end
    
    # spec/spec_helper.rb
    config.before(:each) do
      Prosopite.scan
    end
    
    config.after(:each) do
      Prosopite.finish
    end
  2. Use Prosopite Sidekiq Middleware

    main

    For Sidekiq 6.5.0+, you can auto-detect N+1 queries within Sidekiq jobs by adding the middleware to your Sidekiq initializer.

    Sidekiq.configure_server do |config|
      unless Rails.env.production?
        config.server_middleware do |chain|
          require 'prosopite/middleware/sidekiq'
          chain.add(Prosopite::Middleware::Sidekiq)
        end
      end
    end

    For Sidekiq versions < 6.5.0, guard the implementation:

    if Sidekiq::VERSION >= '6.5.0' && (Rails.env.development? || Rails.env.test?)
      # ... middleware configuration ...
    end
    Sidekiq.configure_server do |config|
      unless Rails.env.production?
        config.server_middleware do |chain|
          require 'prosopite/middleware/sidekiq'
          chain.add(Prosopite::Middleware::Sidekiq)
        end
      end
    end
  3. Install Prosopite

    main

    Add prosopite to your Gemfile. If you are not using MySQL or MariaDB, you must also include the pg_query gem to support PostgreSQL.

    # In Gemfile
    gem 'prosopite'
    gem 'pg_query' # Required for PostgreSQL
    
    # Then run
    $ bundle install

    Alternatively, install via gem command:

    $ gem install prosopite
  4. Enable N+1 detection in Rails Controllers

    main

    To automatically detect N+1 queries in all controllers (except production), use an around_action in your ApplicationController to wrap requests with Prosopite.scan and Prosopite.finish.

    class ApplicationController < ActionController::Base
      unless Rails.env.production?
        around_action :n_plus_one_detection
    
        def n_plus_one_detection
          Prosopite.scan
          yield
        ensure
          Prosopite.finish
        end
      end
    end
    class ApplicationController < ActionController::Base
      unless Rails.env.production?
        around_action :n_plus_one_detection
    
        def n_plus_one_detection
          Prosopite.scan
          yield
        ensure
          Prosopite.finish
        end
      end
    end
  5. Use Prosopite Rack Middleware

    main

    Instead of using around_action in controllers, you can use Rack middleware to detect N+1 queries for all controllers. Add this to config/initializers/prosopite.rb:

    unless Rails.env.production?
      require 'prosopite/middleware/rack'
      Rails.configuration.middleware.use(Prosopite::Middleware::Rack)
    end
    unless Rails.env.production?
      require 'prosopite/middleware/rack'
      Rails.configuration.middleware.use(Prosopite::Middleware::Rack)
    end
  6. Configure Prosopite settings

    main

    Prosopite provides several configuration options to control detection sensitivity and notification channels:

    OptionDescription
    Prosopite.min_n_queriesMinimum number of N queries to report per N+1 case. Defaults to 2.
    Prosopite.raiseRaise warnings as exceptions. Defaults to false.
    Prosopite.start_raiseRaises warnings as exceptions from when this is called. Overrides Prosopite.raise.
    Prosopite.stop_raiseDisables raising warnings as exceptions if previously enabled with Prosopite.start_raise.
    Prosopite.local_raise?Returns true if Prosopite.start_raise has been called previously.
    Prosopite.rails_loggerSend warnings to the Rails log. Defaults to false.
    Prosopite.prosopite_loggerSend warnings to log/prosopite.log. Defaults to false.
    Prosopite.stderr_loggerSend warnings to STDERR. Defaults to false.
    Prosopite.backtrace_cleanerUse a different ActiveSupport::BacktraceCleaner. Defaults to Rails.backtrace_cleaner.
    Prosopite.custom_loggerSet a custom logger. Defaults to false.
    Prosopite.enabledEnables or disables the gem. Defaults to true.
  7. Configure custom logging

    main

    If you need to change how logs are formatted (e.g., to avoid red ANSI escape codes in JSON logs) or send them to a specific destination, use Prosopite.custom_logger.

    # Use the standard Rails logger (removes red highlights)
    Prosopite.custom_logger = Rails.logger
    
    # Use a completely custom logger instance
    Prosopite.custom_logger = MyLoggerClass.new
    Prosopite.custom_logger = Rails.logger
  8. Configure Allow lists for queries and call stacks

    main

    You can ignore specific N+1 notifications using allow lists:

    • Prosopite.allow_stack_paths: Ignore notifications if the call stack contains specific substrings or regexes.
    • Prosopite.ignore_queries: Ignore notifications matching specific SQL queries (regex or exact string).
    Prosopite.allow_stack_paths = ['substring_in_call_stack', /regex/]
    Prosopite.ignore_queries = [/regex_match/, "SELECT * from EXACT_STRING_MATCH"]
    Prosopite.allow_stack_paths = ['substring_in_call_stack', /regex/]
    Prosopite.ignore_queries = [/regex_match/, "SELECT * from EXACT_STRING_MATCH"]
  9. How Prosopite scanning and pausing works

    main

    Prosopite uses a thread-local state to manage scanning. This allows you to control exactly when detection is active.

    • Prosopite.scan: Wraps a block in a scanning session. It automatically starts and finishes the scan, cleaning up state after the block executes.
    • Prosopite.pause: Temporarily disables scanning for the duration of a block. This is useful if you are running code that you know contains N+1 queries but you don't want them reported.
    • Prosopite.resume: Manually resumes scanning if it was paused.

    Note: Prosopite.scan is thread-safe as it uses Thread.current to track the scanning state.

  10. Pause and resume scans

    main

    You can pause detection during specific parts of a scan (e.g., when running background jobs inline) and resume it later. If you use the block form of Prosopite.pause, Prosopite.resume is called automatically.

    Manual pause/resume:

    Prosopite.scan
    # <code to scan>
    Prosopite.pause
    # <code that has n+1s (will be ignored)
    Prosopite.resume
    Prosopite.finish

    Block form:

    Prosopite.scan
    # <code to scan>
    result = Prosopite.pause do
      # <code that has n+1s
    end
    Prosopite.finish

    To ensure paused items are still reported, set Prosopite.ignore_pauses = true.

    result = Prosopite.pause do
      # <code that has n+1s
    end
  11. Scan arbitrary code blocks

    main

    To scan code outside of controllers or tests, wrap the code with Prosopite.scan and Prosopite.finish. You can also use the block form, which automatically calls finish at the end of the block and returns the block's result.

    Manual wrap:

    Prosopite.scan
    <code to scan>
    Prosopite.finish

    Block form (recommended):

    my_object = Prosopite.scan do
      MyObjectFactory.create(params)
    end
    my_object = Prosopite.scan do
      MyObjectFactory.create(params)
    end
  12. Configure N+1 query reporting and error raising

    main

    Prosopite allows you to choose how N+1 detections are communicated. You can log to various outputs or raise a Prosopite::NPlusOneQueriesError.

    Error Raising:

    • Prosopite.raise=: If set to true, Prosopite will raise a Prosopite::NPlusOneQueriesError when N+1 queries are detected.
    • Prosopite.start_raise / Prosopite.stop_raise: Methods to toggle error raising locally within a specific scope.

    Logging: Set these to true to enable logging to the respective outputs:

    • Prosopite.stderr_logger = true: Logs to $stderr (output is colorized in red).
    • Prosopite.rails_logger = true: Logs to Rails.logger (output is colorized in red).
    • Prosopite.prosopite_logger = true: Logs to log/prosopite.log in your Rails root.
    • Prosopite.custom_logger = logger_instance: Pass a custom logger instance to use warn for notifications.