├── .github └── workflows │ └── ruby.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── examples ├── console.jpg └── slack.jpg ├── lib ├── query_track.rb ├── query_track │ ├── event_processor.rb │ ├── filters.rb │ ├── notifications │ │ ├── custom.rb │ │ ├── log.rb │ │ └── slack.rb │ ├── settings.rb │ ├── trace.rb │ └── version.rb └── subscriber.rb ├── query_track.gemspec └── spec ├── query_track ├── event_processor_spec.rb ├── filters_spec.rb ├── notifications │ ├── custom_spec.rb │ └── slack_spec.rb └── trace_spec.rb ├── query_track_spec.rb └── spec_helper.rb /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | include: 10 | - ruby: 3.3 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: ruby/setup-ruby@v1 14 | with: 15 | ruby-version: ${{ matrix.ruby }} 16 | bundler-cache: true 17 | - run: bundle exec rspec -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | DisplayCopNames: true 3 | DisplayStyleGuide: true 4 | StyleGuideCopsOnly: true 5 | TargetRubyVersion: 2.5 6 | 7 | Metrics/LineLength: 8 | Max: 120 9 | 10 | Documentation: 11 | Enabled: false 12 | 13 | FrozenStringLiteralComment: 14 | Enabled: false 15 | 16 | Style/FileName: 17 | Enabled: false 18 | 19 | Metrics/ModuleLength: 20 | Exclude: 21 | - spec/**/*_spec.rb 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.0.10 2020-08-07 2 | 3 | Added custom app directory ([evanboho](https://github.com/kirillshevch/query_track/pull/60)) 4 | 5 | # v0.0.9 2019-12-21 6 | 7 | Added custom handles ([kirillshevch](https://github.com/kirillshevch/query_track/pull/29)) 8 | 9 | # v0.0.8 2019-08-17 10 | 11 | Added enabled/disable config ([kirillshevch](https://github.com/kirillshevch/query_track/pull/13)) 12 | 13 | # v0.0.7 2019-08-01 14 | 15 | fixed view of duration seconds ([kirillshevch](https://github.com/kirillshevch/query_track/pull/8)) 16 | 17 | # v0.0.6 2019-08-01 18 | 19 | repush of 0.0.5 20 | 21 | # v0.0.5 2019-08-01 22 | 23 | *yanked* 24 | 25 | # v0.0.5 2019-07-31 26 | 27 | Use #find instead of #map .include?(false). ([kirillshevch](https://github.com/kirillshevch/query_track/pull/7)) 28 | 29 | # v0.0.4 2019-07-22 30 | 31 | Added temp solution for async slack notifications. ([kirillshevch](https://github.com/kirillshevch/query_track/pull/5)) 32 | 33 | # v0.0.3 2019-07-22 34 | 35 | Added backtrace filters. ([kirillshevch](https://github.com/kirillshevch/query_track/pull/3)) 36 | 37 | # v0.0.2 2019-07-22 38 | 39 | Add console log, fix disabling of slack notifications. ([kirillshevch](https://github.com/kirillshevch/query_track/pull/2)) 40 | 41 | # v0.0.1 2019-07-20 42 | 43 | Initial release with event processing, slack notifications and duration limit. ([kirillshevch](https://github.com/kirillshevch/query_track/pull/1)) 44 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # Specify your gem's dependencies in query_track.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | query_track (0.0.14) 5 | activesupport 6 | dry-configurable 7 | slack_hook 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | activesupport (7.1.3) 13 | base64 14 | bigdecimal 15 | concurrent-ruby (~> 1.0, >= 1.0.2) 16 | connection_pool (>= 2.2.5) 17 | drb 18 | i18n (>= 1.6, < 2) 19 | minitest (>= 5.1) 20 | mutex_m 21 | tzinfo (~> 2.0) 22 | ast (2.4.2) 23 | base64 (0.2.0) 24 | bigdecimal (3.1.6) 25 | byebug (11.1.3) 26 | concurrent-ruby (1.2.3) 27 | connection_pool (2.4.1) 28 | diff-lcs (1.5.1) 29 | drb (2.2.0) 30 | ruby2_keywords 31 | dry-configurable (1.1.0) 32 | dry-core (~> 1.0, < 2) 33 | zeitwerk (~> 2.6) 34 | dry-core (1.0.1) 35 | concurrent-ruby (~> 1.0) 36 | zeitwerk (~> 2.6) 37 | i18n (1.14.1) 38 | concurrent-ruby (~> 1.0) 39 | json (2.7.1) 40 | language_server-protocol (3.17.0.3) 41 | minitest (5.22.2) 42 | mutex_m (0.2.0) 43 | parallel (1.24.0) 44 | parser (3.3.0.5) 45 | ast (~> 2.4.1) 46 | racc 47 | racc (1.7.3) 48 | rainbow (3.1.1) 49 | rake (13.1.0) 50 | regexp_parser (2.9.0) 51 | rexml (3.2.6) 52 | rspec (3.13.0) 53 | rspec-core (~> 3.13.0) 54 | rspec-expectations (~> 3.13.0) 55 | rspec-mocks (~> 3.13.0) 56 | rspec-core (3.13.0) 57 | rspec-support (~> 3.13.0) 58 | rspec-expectations (3.13.0) 59 | diff-lcs (>= 1.2.0, < 2.0) 60 | rspec-support (~> 3.13.0) 61 | rspec-mocks (3.13.0) 62 | diff-lcs (>= 1.2.0, < 2.0) 63 | rspec-support (~> 3.13.0) 64 | rspec-support (3.13.0) 65 | rubocop (1.60.2) 66 | json (~> 2.3) 67 | language_server-protocol (>= 3.17.0) 68 | parallel (~> 1.10) 69 | parser (>= 3.3.0.2) 70 | rainbow (>= 2.2.2, < 4.0) 71 | regexp_parser (>= 1.8, < 3.0) 72 | rexml (>= 3.2.5, < 4.0) 73 | rubocop-ast (>= 1.30.0, < 2.0) 74 | ruby-progressbar (~> 1.7) 75 | unicode-display_width (>= 2.4.0, < 3.0) 76 | rubocop-ast (1.30.0) 77 | parser (>= 3.2.1.0) 78 | ruby-progressbar (1.13.0) 79 | ruby2_keywords (0.0.5) 80 | slack_hook (0.1.2) 81 | tzinfo (2.0.6) 82 | concurrent-ruby (~> 1.0) 83 | unicode-display_width (2.5.0) 84 | zeitwerk (2.6.13) 85 | 86 | PLATFORMS 87 | ruby 88 | 89 | DEPENDENCIES 90 | bundler 91 | byebug 92 | query_track! 93 | rake 94 | rspec 95 | rubocop 96 | 97 | BUNDLED WITH 98 | 2.4.19 99 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Kirill Shevchenko 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 | # QueryTrack 2 | 3 | [![Gem Version](https://badge.fury.io/rb/query_track.svg)](https://badge.fury.io/rb/query_track) 4 | [![Maintainability](https://api.codeclimate.com/v1/badges/15b0a6c0a1838b216db8/maintainability)](https://codeclimate.com/github/kirillshevch/query_track/maintainability) 5 | 6 | Tool for finding time-consuming database queries for ActiveRecord-based Rails Apps. Provides Slack notifications with backtrace, raw SQL, time duration, etc. 7 | 8 | ## Installation 9 | 10 | Add this line to your application's `Gemfile` and then execute `bundle install`: 11 | 12 | ```ruby 13 | gem 'query_track' 14 | ``` 15 | 16 | ## Usage 17 | 18 | ### SQL Duration Limit 19 | 20 | Specify SQL duration query limit (in seconds): 21 | 22 | ```ruby 23 | QueryTrack::Settings.configure do |config| 24 | config.duration = 0.5 25 | end 26 | ``` 27 | 28 | ### Console Log 29 | 30 | Enable console logs from config: 31 | 32 | ```ruby 33 | QueryTrack::Settings.configure do |config| 34 | config.duration = 0.5 35 | config.logs = true 36 | end 37 | ``` 38 | 39 | # Log Example 40 | 41 | ### Filters 42 | 43 | To avoid noisy warnings from used gems, and places where fat queries are justified, you can filters SQL by backtrace. 44 | For example, you have installed `activeadmin` and want to skip everything from `app/admin`: 45 | 46 | ```ruby 47 | QueryTrack::Settings.configure do |config| 48 | config.duration = 0.5 49 | config.filters = ['app/admin'] 50 | end 51 | ``` 52 | ### App Directory 53 | 54 | QueryTrack finds the trace by filtering the caller by the app directory. 55 | By default, the app directory is set to 'app', the default for Rails apps. 56 | For apps that have a non-stanard app directory, this can be set with the `app_dir` config field: 57 | 58 | ```ruby 59 | QueryTrack::Settings.configure do |config| 60 | config.duration = 0.5 61 | config.app_dir = 'backend' 62 | end 63 | ``` 64 | 65 | ### Enable/Disable toggle 66 | 67 | Enable/disable with ENV variables to turn it on/off without code push. By default *QueryTrack* is enabled. 68 | 69 | ```ruby 70 | QueryTrack::Settings.configure do |config| 71 | config.duration = 0.5 72 | config.enabled = ENV['QUERY_TRACK_ENABLED'] 73 | end 74 | ``` 75 | 76 | ### Slack Notifications 77 | 78 | To receive notifications about slow queries into Slack, you need to install [incoming-webhooks](https://slack.com/apps/A0F7XDUAZ-incoming-webhooks) and put link into config file: 79 | 80 | ```ruby 81 | QueryTrack::Settings.configure do |config| 82 | config.duration = 0.5 83 | config.notifications.slack = 'https://hooks.slack.com/services/T0000000/B0000000/C0000000' 84 | end 85 | ``` 86 | 87 | # Incoming Hook Example 88 | 89 | ## Custom Notifications (Handlers) 90 | 91 | You can write your own handler for slow queries. Send data to any source(for e.g. to logs storage) or make notification for another source(Email, Messengers, etc.) 92 | 93 | ```ruby 94 | QueryTrack::Settings.configure do |config| 95 | config.duration = 0.5 96 | config.notifications.custom_handler = -> (sql, duration, trace) { 97 | # data processing... 98 | } 99 | end 100 | ``` 101 | 102 | ## Production Usage Notes 103 | 104 | When [QueryTrack](https://github.com/kirillshevch/query_track/blob/master/lib/query_track/notifications/slack.rb#L21) send slack hooks, request is executed in separate thread. So there should be no synchronous delays. 105 | 106 | Subscription to SQL events and checking duration time actually take insignificant time in milliseconds. 107 | 108 | If your project is horizontally scaled, you can install `query_track` for one of the node to avoid performance degrade for whole application. 109 | 110 | ## Contributing 111 | 112 | Bug reports and pull requests are welcome on GitHub at https://github.com/kirillshevch/query_track. 113 | 114 | ## License 115 | 116 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 117 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'query_track' 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require 'irb' 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /examples/console.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirillshevch/query_track/15261a340e0b61a805a629247bb9b681d83c143f/examples/console.jpg -------------------------------------------------------------------------------- /examples/slack.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirillshevch/query_track/15261a340e0b61a805a629247bb9b681d83c143f/examples/slack.jpg -------------------------------------------------------------------------------- /lib/query_track.rb: -------------------------------------------------------------------------------- 1 | require 'dry-configurable' 2 | require 'active_support/notifications' 3 | require 'slack_hook' 4 | require 'query_track/version' 5 | require 'query_track/settings' 6 | require 'query_track/trace' 7 | require 'query_track/filters' 8 | require 'query_track/notifications/custom' 9 | require 'query_track/notifications/slack' 10 | require 'query_track/notifications/log' 11 | require 'query_track/event_processor' 12 | require 'subscriber' 13 | require 'logger' 14 | 15 | module QueryTrack 16 | class << self 17 | attr_writer :logger 18 | 19 | def logger 20 | @logger ||= Logger.new($stdout).tap do |log| 21 | log.progname = self.name 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/query_track/event_processor.rb: -------------------------------------------------------------------------------- 1 | module QueryTrack 2 | class EventProcessor 3 | attr_reader :event, :subscriber 4 | 5 | def initialize(event, subscriber) 6 | @event = event 7 | @subscriber = subscriber 8 | end 9 | 10 | def call 11 | unsubscribe 12 | return unless QueryTrack::Settings.config.duration && QueryTrack::Settings.config.enabled 13 | 14 | return if under_filter?(caller) 15 | 16 | if duration_seconds > QueryTrack::Settings.config.duration 17 | QueryTrack::Notifications::Slack.new(event.payload[:sql], duration_seconds).call 18 | QueryTrack::Notifications::Log.new(event.payload[:sql], duration_seconds).call 19 | QueryTrack::Notifications::Custom.new(event.payload[:sql], duration_seconds).call 20 | end 21 | end 22 | 23 | private 24 | 25 | def unsubscribe 26 | return if QueryTrack::Settings.config.enabled 27 | 28 | ActiveSupport::Notifications.unsubscribe(subscriber) 29 | end 30 | 31 | def duration_seconds 32 | event.duration / 1000 33 | end 34 | 35 | def under_filter?(trace) 36 | QueryTrack::Filters.new(caller).call 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/query_track/filters.rb: -------------------------------------------------------------------------------- 1 | module QueryTrack 2 | class Filters 3 | attr_reader :full_trace 4 | 5 | def initialize(full_trace) 6 | @full_trace = full_trace 7 | end 8 | 9 | def call 10 | QueryTrack::Settings.config.filters.find do |filter| 11 | full_trace.select { |v| v =~ %r{#{filter}} }[0] 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/query_track/notifications/custom.rb: -------------------------------------------------------------------------------- 1 | module QueryTrack 2 | module Notifications 3 | class Custom 4 | attr_reader :code, :duration 5 | 6 | def initialize(code, duration) 7 | @code = code.strip 8 | @duration = duration 9 | end 10 | 11 | def call 12 | return unless QueryTrack::Settings.config.notifications.custom_handler 13 | 14 | trace = QueryTrack::Trace.new(caller).call 15 | 16 | QueryTrack::Settings.config.notifications.custom_handler.call(code, duration, trace) 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/query_track/notifications/log.rb: -------------------------------------------------------------------------------- 1 | module QueryTrack 2 | module Notifications 3 | class Log 4 | attr_reader :code, :duration 5 | 6 | def initialize(code, duration) 7 | @code = code.strip 8 | @duration = duration 9 | end 10 | 11 | def call 12 | return unless QueryTrack::Settings.config.logs 13 | 14 | trace = QueryTrack::Trace.new(caller).call 15 | 16 | QueryTrack.logger.info "\n#{code}\nDuration: #{duration}s\nTrace: #{trace}" 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/query_track/notifications/slack.rb: -------------------------------------------------------------------------------- 1 | module QueryTrack 2 | module Notifications 3 | class Slack 4 | attr_reader :code, :duration, :webhook_url 5 | 6 | def initialize(code, duration) 7 | @code = code.strip 8 | @duration = duration 9 | @webhook_url = QueryTrack::Settings.config.notifications.slack 10 | end 11 | 12 | def call 13 | return if webhook_url.nil? || webhook_url.empty? 14 | 15 | slack_hook = SlackHook::Incoming.new(webhook_url) 16 | 17 | trace = QueryTrack::Trace.new(caller).call 18 | 19 | payload = { blocks: blocks(trace) } 20 | 21 | Thread.new { slack_hook.post(payload) } 22 | end 23 | 24 | private 25 | 26 | def blocks(trace) 27 | [ 28 | { 29 | "type": 'divider' 30 | }, 31 | { 32 | "type": 'section', 33 | "text": { 34 | "type": 'mrkdwn', 35 | "text": "```#{code}```" 36 | } 37 | }, 38 | { 39 | "type": 'context', 40 | "elements": [ 41 | { 42 | "type": 'mrkdwn', 43 | "text": "Duration: *#{duration}s* " 44 | } 45 | ] 46 | }, 47 | { 48 | "type": 'context', 49 | "elements": [ 50 | { 51 | "type": 'mrkdwn', 52 | "text": "#{trace}" 53 | } 54 | ] 55 | } 56 | ] 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/query_track/settings.rb: -------------------------------------------------------------------------------- 1 | module QueryTrack 2 | class Settings 3 | extend ::Dry::Configurable 4 | 5 | setting :duration 6 | 7 | setting :notifications do 8 | setting :slack, default: '' 9 | setting :custom_handler 10 | end 11 | 12 | setting :logs, default: false 13 | 14 | setting :filters, default: [] 15 | 16 | setting :enabled, default: true 17 | 18 | setting :app_dir, default: 'app' 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/query_track/trace.rb: -------------------------------------------------------------------------------- 1 | module QueryTrack 2 | class Trace 3 | attr_reader :full_trace 4 | 5 | def initialize(full_trace) 6 | @full_trace = full_trace 7 | end 8 | 9 | def call 10 | full_trace.select { |v| v =~ %r{#{QueryTrack::Settings.config.app_dir}/} }[0] 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/query_track/version.rb: -------------------------------------------------------------------------------- 1 | module QueryTrack 2 | VERSION = '0.0.14' 3 | end 4 | -------------------------------------------------------------------------------- /lib/subscriber.rb: -------------------------------------------------------------------------------- 1 | subscriber = ActiveSupport::Notifications.subscribe('sql.active_record') do |*args| 2 | event = ActiveSupport::Notifications::Event.new(*args) 3 | 4 | QueryTrack::EventProcessor.new(event, subscriber).call 5 | end 6 | -------------------------------------------------------------------------------- /query_track.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib', __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'query_track/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'query_track' 7 | spec.version = QueryTrack::VERSION 8 | spec.authors = ['Kirill Shevchenko'] 9 | spec.email = ['hello@kirillshevch.com'] 10 | 11 | spec.summary = 'Finding time-consuming database queries for ActiveRecord-based Rails Apps' 12 | spec.description = spec.summary 13 | spec.homepage = 'https://github.com/kirillshevch/query_track' 14 | spec.license = 'MIT' 15 | 16 | # Specify which files should be added to the gem when it is released. 17 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 18 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 19 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 20 | end 21 | spec.bindir = 'exe' 22 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 23 | spec.require_paths = ['lib'] 24 | 25 | spec.add_dependency 'activesupport' 26 | spec.add_dependency 'slack_hook' 27 | spec.add_dependency 'dry-configurable' 28 | 29 | spec.add_development_dependency 'bundler' 30 | spec.add_development_dependency 'rubocop' 31 | spec.add_development_dependency 'rake' 32 | spec.add_development_dependency 'rspec' 33 | spec.add_development_dependency 'byebug' 34 | end 35 | -------------------------------------------------------------------------------- /spec/query_track/event_processor_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe QueryTrack::EventProcessor do 2 | let(:payload) do ['sql.active_record', Time.now, Time.now, '1e07319', 3 | { 4 | sql: "SET client_min_messages TO 'warning'", 5 | name: 'SCHEMA', 6 | binds: [], 7 | type_casted_binds: [], 8 | statement_name: nil, 9 | connection_id: 1 10 | } 11 | ] end 12 | 13 | let!(:event) { ActiveSupport::Notifications::Event.new(*payload) } 14 | let(:subscriber) { double } 15 | 16 | subject { described_class.new(event, subscriber) } 17 | 18 | context 'skipping case' do 19 | it 'should return if duration not specified' do 20 | expect(event).not_to receive(:duration) 21 | subject.call 22 | end 23 | end 24 | 25 | context 'processing' do 26 | before do 27 | QueryTrack::Settings.configure do |config| 28 | config.duration = 1.0 29 | config.logs = true 30 | end 31 | end 32 | 33 | it 'should process event if duration specified' do 34 | allow(event).to receive(:duration).and_return(1) 35 | expect(event).to receive(:duration) 36 | subject.call 37 | end 38 | 39 | it 'should call logs if enabled' do 40 | allow(event).to receive(:duration).and_return(5000) # value in milliseconds 41 | mock_logger = double('log') 42 | allow(mock_logger).to receive(:call) 43 | expect(QueryTrack::Notifications::Log).to receive(:new).and_return(mock_logger) 44 | subject.call 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/query_track/filters_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe QueryTrack::Filters do 2 | let(:fake_trace) { ['app/admin/user.rb', 'lib/config/query_track.rb'] } 3 | 4 | context 'has content under filter' do 5 | before do 6 | QueryTrack::Settings.configure do |config| 7 | config.duration = 1.0 8 | config.filters = ['app/admin'] 9 | end 10 | end 11 | 12 | it 'should return true' do 13 | expect(QueryTrack::Filters.new(fake_trace).call).to be_truthy 14 | end 15 | end 16 | 17 | context 'has no content under filter' do 18 | before do 19 | QueryTrack::Settings.configure do |config| 20 | config.duration = 1.0 21 | config.filters = ['app/models'] 22 | end 23 | end 24 | 25 | it 'should return true' do 26 | expect(QueryTrack::Filters.new(fake_trace).call).to be_falsey 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/query_track/notifications/custom_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe QueryTrack::Notifications::Custom do 2 | let(:code) { 'COMMIT' } 3 | let(:duration) { 5 } 4 | 5 | subject { described_class.new(code, duration) } 6 | 7 | context 'custom handler not specified' do 8 | before do 9 | QueryTrack::Settings.configure do |config| 10 | config.duration = 1.0 11 | end 12 | end 13 | 14 | it 'should not return anything' do 15 | expect(subject.call).to be_nil 16 | end 17 | end 18 | 19 | context 'custom handler specified' do 20 | before do 21 | QueryTrack::Settings.configure do |config| 22 | config.duration = 1.0 23 | config.notifications.custom_handler = -> (sql, duration, trace) {} 24 | end 25 | end 26 | 27 | it 'should call custom handler' do 28 | expect(QueryTrack::Settings.config.notifications.custom_handler).to receive(:call) 29 | subject.call 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/query_track/notifications/slack_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe QueryTrack::Notifications::Slack do 2 | let(:code) { 'COMMIT' } 3 | let(:duration) { 5 } 4 | let(:slack_stub) { double } 5 | 6 | subject { described_class.new(code, duration) } 7 | 8 | context 'slack not specified' do 9 | before do 10 | QueryTrack::Settings.configure do |config| 11 | config.duration = 1.0 12 | end 13 | end 14 | 15 | it 'should post notification to slack' do 16 | #allow(SlackHook::Incoming).to receive(:new).and_return(slack_stub) 17 | expect(slack_stub).not_to receive(:post) 18 | subject.call 19 | end 20 | end 21 | 22 | context 'slack specified' do 23 | before do 24 | QueryTrack::Settings.configure do |config| 25 | config.duration = 1.0 26 | config.notifications.slack = 'https://hooks.slack.com/services/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' 27 | end 28 | end 29 | 30 | it 'should post notification to slack' do 31 | allow(SlackHook::Incoming).to receive(:new).and_return(slack_stub) 32 | expect(Thread).to receive(:new) 33 | subject.call 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/query_track/trace_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe QueryTrack::Trace do 2 | let(:app_dir_path) { 'app/admin/user.rb' } 3 | let(:fake_trace) { [app_dir_path, 'lib/config/query_track.rb'] } 4 | 5 | before do 6 | QueryTrack::Settings.configure do |config| 7 | config.duration = 1.0 8 | end 9 | end 10 | 11 | context 'has default app directory' do 12 | it 'should return path matching app directory' do 13 | expect(QueryTrack::Trace.new(fake_trace).call).to eq app_dir_path 14 | end 15 | end 16 | 17 | context 'has override app directory' do 18 | let(:app_dir_path) { 'backend/models/user.rb' } 19 | 20 | before do 21 | QueryTrack::Settings.configure do |config| 22 | config.app_dir = 'backend' 23 | end 24 | end 25 | 26 | it 'should return path matching app directory' do 27 | expect(QueryTrack::Trace.new(fake_trace).call).to eq app_dir_path 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/query_track_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe QueryTrack do 2 | it 'has a version number' do 3 | expect(QueryTrack::VERSION).not_to be nil 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'query_track' 3 | 4 | RSpec.configure do |config| 5 | # Enable flags like --only-failures and --next-failure 6 | config.example_status_persistence_file_path = '.rspec_status' 7 | 8 | # Disable RSpec exposing methods globally on `Module` and `main` 9 | config.disable_monkey_patching! 10 | 11 | config.expect_with :rspec do |c| 12 | c.syntax = :expect 13 | end 14 | end 15 | --------------------------------------------------------------------------------