sql_tracker 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c0e4058d31c7fa127cfe022ef763b942c8835187
4
+ data.tar.gz: e8ae58b2c8e734631444a810a8d780fa8e52f520
5
+ SHA512:
6
+ metadata.gz: 2db4e2a2060c9fae17014d38d166d4fac55eeaa2c9377ddb64043abf81ef48bb793eefcdd4a826aad0fa755972d8a30d9542a6b6aec311f62cc76dc2efc6e8d3
7
+ data.tar.gz: ccdee35b3d94e968c541c827e7340bcd97542a4c060ca7800cf98c93512e1a7dc3349dad7993d985ff599c945f671db332c64758b672758560d073d40551b089
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ .DS_Store
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.0
5
+ before_install: gem install bundler -v 1.12.4
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Steven Yue
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.
data/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # Rails SQL Query Tracker
2
+
3
+ `sql_tracker` tracks SQL queries by subscribing to Rails' `sql.active_record` event notifications.
4
+
5
+ It then aggregates and generates report to give you insights about all the sql queries happened in your Rails application.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ group :development, :test do
13
+ ... ...
14
+ gem 'sql_tracker'
15
+ end
16
+ ```
17
+
18
+ And then execute:
19
+
20
+ $ bundle
21
+
22
+
23
+ ## Tracking
24
+
25
+ To start tracking, simply start your rails application server. When your server is shutting down, `sql_tracker` will dump all the tracking data into one or more json file(s) under the `tmp` folder of your application.
26
+
27
+ `sql_tracker` can also track sql queries when running rails tests (e.g. your controller or integration tests), it will dump the data after all the tests are finished.
28
+
29
+
30
+ ## Reporting
31
+
32
+ To generate report, run
33
+ ```bash
34
+ sql_tracker tmp/sql_tracker-*.json
35
+ ```
36
+
37
+ ## License
38
+
39
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
40
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << 'test'
6
+ t.libs << 'lib'
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task default: :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'sql_tracker'
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
data/bin/setup ADDED
@@ -0,0 +1,8 @@
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
data/bin/sql_tracker ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env ruby
2
+ require 'sql_tracker/report'
3
+ require 'json'
4
+
5
+ reports = []
6
+ until ARGV.empty?
7
+ begin
8
+ file = ARGV.pop
9
+ report = SqlTracker::Report.new(JSON.load(IO.binread(file)))
10
+ if report.valid?
11
+ reports << report
12
+ else
13
+ STDOUT.puts "Skip incompatible file: #{file}"
14
+ end
15
+ rescue => e
16
+ STDERR.puts "Error when parsing #{file}: #{e.inspect}"
17
+ end
18
+ end
19
+
20
+ if reports.empty?
21
+ STDERR.puts 'Unable to find sql_tracker json dump to generate report'
22
+ exit
23
+ end
24
+
25
+ report = reports.inject(:+)
26
+ report.print_text
@@ -0,0 +1,20 @@
1
+ require 'sql_tracker/config'
2
+ require 'sql_tracker/handler'
3
+ require 'sql_tracker/report'
4
+
5
+ module SqlTracker
6
+ def self.initialize!
7
+ raise 'sql tracker initialized twice' if @already_initialized
8
+
9
+ config = SqlTracker::Config.apply_defaults
10
+ handler = SqlTracker::Handler.new(config)
11
+ ActiveSupport::Notifications.subscribe('sql.active_record', handler)
12
+ @already_initialized = true
13
+
14
+ at_exit { handler.save }
15
+ end
16
+ end
17
+
18
+ if defined?(::Rails) && ::Rails::VERSION::MAJOR.to_i >= 3
19
+ require 'sql_tracker/railtie'
20
+ end
@@ -0,0 +1,17 @@
1
+ require 'active_support/configurable'
2
+
3
+ module SqlTracker
4
+ class Config
5
+ include ActiveSupport::Configurable
6
+ config_accessor :tracked_paths, :tracked_sql_command, :output_path
7
+
8
+ class << self
9
+ def apply_defaults
10
+ self.tracked_paths ||= %w(app lib)
11
+ self.tracked_sql_command ||= %w(SELECT INSERT UPDATE DELETE)
12
+ self.output_path ||= File.join(Rails.root.to_s, 'tmp')
13
+ self
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,87 @@
1
+ require 'digest/md5'
2
+ require 'json'
3
+ require 'fileutils'
4
+
5
+ module SqlTracker
6
+ class Handler
7
+ def initialize(config)
8
+ @config = config
9
+ @started_at = Time.now.to_s
10
+ @data = {} # {key: {sql:, count:, duration, source: []}, ...}
11
+ end
12
+
13
+ def call(name, started, finished, id, payload)
14
+ sql = payload[:sql]
15
+
16
+ return unless sql.start_with?(*@config.tracked_sql_command)
17
+
18
+ cleaned_trace = clean_trace(caller)
19
+ return if cleaned_trace.empty?
20
+
21
+ sql = clean_sql_query(sql)
22
+ duration = 1000.0 * (finished - started) # in milliseconds
23
+ sql_key = Digest::MD5.hexdigest(sql)
24
+
25
+ if @data.key?(sql_key)
26
+ update_data(sql_key, cleaned_trace, duration)
27
+ else
28
+ add_data(sql_key, sql, cleaned_trace, duration)
29
+ end
30
+ end
31
+
32
+ def clean_sql_query(query)
33
+ query.squish!
34
+ query.gsub!(/(\s(=|>|<|>=|<=|<>|!=)\s)('[^']+'|\w+)/, '\1xxx')
35
+ query.gsub!(/(\sIN\s)\([^\(\)]+\)/, '\1(xxx)')
36
+ query.gsub!(/BETWEEN ('[^']+'|\w+) AND ('[^']+'|\w+)/, 'BETWEEN xxx AND xxx')
37
+ query
38
+ end
39
+
40
+ def clean_trace(trace)
41
+ if Rails.backtrace_cleaner.instance_variable_get(:@root) == '/'
42
+ Rails.backtrace_cleaner.instance_variable_set :@root, Rails.root.to_s
43
+ end
44
+
45
+ Rails.backtrace_cleaner.remove_silencers!
46
+ Rails.backtrace_cleaner.add_silencer do |line|
47
+ line !~ %r{^(#{@config.tracked_paths.join('|')})\/}
48
+ end
49
+ Rails.backtrace_cleaner.clean(trace)
50
+ end
51
+
52
+ def add_data(key, sql, trace, duration)
53
+ @data[key] = {}
54
+ @data[key][:sql] = sql
55
+ @data[key][:count] = 1
56
+ @data[key][:duration] = duration
57
+ @data[key][:source] = [trace.first]
58
+ @data
59
+ end
60
+
61
+ def update_data(key, trace, duration)
62
+ @data[key][:count] += 1
63
+ @data[key][:duration] += duration
64
+ @data[key][:source] << trace.first
65
+ @data
66
+ end
67
+
68
+ # save the data to file
69
+ def save
70
+ return if @data.empty?
71
+ output = {}
72
+ output[:data] = @data
73
+ output[:generated_at] = Time.now.to_s
74
+ output[:started_at] = @started_at
75
+ output[:format_version] = '1.0'
76
+ output[:rails_version] = Rails.version
77
+ output[:rails_path] = Rails.root.to_s
78
+
79
+ FileUtils.mkdir_p(@config.output_path)
80
+ filename = "sql_tracker-#{Process.pid}-#{Time.now.to_i}.json"
81
+
82
+ File.open(File.join(@config.output_path, filename), 'w') do |f|
83
+ f.write JSON.dump(output)
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,7 @@
1
+ module SqlTracker
2
+ class Railtie < ::Rails::Railtie
3
+ initializer 'sql_tracker.configure_rails_initialization' do
4
+ SqlTracker.initialize!
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,140 @@
1
+ module SqlTracker
2
+ class Report
3
+ attr_accessor :raw_data
4
+
5
+ def initialize(data)
6
+ self.raw_data = data
7
+ end
8
+
9
+ def valid?
10
+ return false unless raw_data.key?('format_version')
11
+ return false unless raw_data.key?('data')
12
+ return false if raw_data['data'].nil? || raw_data['data'].empty?
13
+ sample = raw_data['data'].values.first
14
+ %w(sql count source).each do |key|
15
+ return false unless sample.key?(key)
16
+ end
17
+ true
18
+ end
19
+
20
+ def version
21
+ raw_data['format_version']
22
+ end
23
+
24
+ def data
25
+ raw_data['data']
26
+ end
27
+
28
+ def print_text(f = STDOUT)
29
+ f.puts '=================================='
30
+ f.puts "Total Unique SQL Queries: #{data.keys.size}"
31
+ f.puts '=================================='
32
+ f.printf(
33
+ "%-#{count_width}s | %-#{duration_width}s | %-#{sql_width}s | Source\n",
34
+ 'Count', 'Avg Time (ms)', 'SQL Query'
35
+ )
36
+ f.puts '-' * terminal_width
37
+
38
+ data.values.sort_by { |d| -d['count'] }.each do |row|
39
+ chopped_sql = wrap_text(row['sql'], sql_width)
40
+ source_list = wrap_list(row['source'].uniq, sql_width - 10)
41
+ avg_duration = row['duration'].to_f / row['count']
42
+ total_lines = if chopped_sql.length >= source_list.length
43
+ chopped_sql.length
44
+ else
45
+ source_list.length
46
+ end
47
+
48
+ (0...total_lines).each do |line|
49
+ count = line == 0 ? row['count'].to_s : ''
50
+ duration = line == 0 ? avg_duration.round(2) : ''
51
+ source = source_list.length > line ? source_list[line] : ''
52
+ query = row['sql'].length > line ? chopped_sql[line] : ''
53
+ f.printf(
54
+ "%-#{count_width}s | %-#{duration_width}s | %-#{sql_width}s | %-#{sql_width}s\n",
55
+ count, duration, query, source
56
+ )
57
+ end
58
+ f.puts '-' * terminal_width
59
+ end
60
+ end
61
+
62
+ def +(other)
63
+ unless self.class == other.class
64
+ raise ArgumentError, "cannot combine #{other.class}"
65
+ end
66
+ unless version == other.version
67
+ raise ArgumentError, "cannot combine v#{version} with v#{other.version}"
68
+ end
69
+
70
+ r1 = data
71
+ r2 = other.data
72
+
73
+ merged = (r1.keys + r2.keys).uniq.each_with_object({}) do |id, memo|
74
+ if !r1.key?(id)
75
+ memo[id] = r2[id]
76
+ elsif r2.key?(id)
77
+ memo[id] = r1[id]
78
+ memo[id]['count'] += r2[id]['count']
79
+ memo[id]['duration'] += r2[id]['duration']
80
+ memo[id]['source'] += r2[id]['source']
81
+ else
82
+ memo[id] = r1[id]
83
+ end
84
+ end
85
+ merged_data = { 'data' => merged, 'format_version' => version }
86
+
87
+ self.class.new(merged_data)
88
+ end
89
+
90
+ private
91
+
92
+ def wrap_text(text, width)
93
+ return [text] if text.length <= width
94
+ text.scan(/.{1,#{width}}/)
95
+ end
96
+
97
+ # an array of text
98
+ def wrap_list(list, width)
99
+ list.map do |text|
100
+ wrap_text(text, width)
101
+ end.flatten
102
+ end
103
+
104
+ def sql_width
105
+ @sql_width ||= (terminal_width - count_width - duration_width) / 2
106
+ end
107
+
108
+ def count_width
109
+ 5
110
+ end
111
+
112
+ def duration_width
113
+ 15
114
+ end
115
+
116
+ def terminal_width
117
+ @terminal_width ||= begin
118
+ result = unix? ? dynamic_width : 80
119
+ result < 10 ? 80 : result
120
+ end
121
+ end
122
+
123
+ def dynamic_width
124
+ @dynamic_width ||= (dynamic_width_stty.nonzero? || dynamic_width_tput)
125
+ end
126
+
127
+ def dynamic_width_stty
128
+ `stty size 2>/dev/null`.split[1].to_i
129
+ end
130
+
131
+ def dynamic_width_tput
132
+ `tput cols 2>/dev/null`.to_i
133
+ end
134
+
135
+ def unix?
136
+ RUBY_PLATFORM =~
137
+ /(aix|darwin|linux|(net|free|open)bsd|cygwin|solaris|irix|hpux)/i
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,3 @@
1
+ module SqlTracker
2
+ VERSION = '1.0.0'.freeze
3
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'sql_tracker/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'sql_tracker'
8
+ spec.version = SqlTracker::VERSION
9
+ spec.authors = ['Steven Yue']
10
+ spec.email = ['jincheker@gmail.com']
11
+
12
+ spec.summary = 'Rails SQL Query Tracker'
13
+ spec.description = 'Track and analyze sql queries of your rails application'
14
+ spec.homepage = 'http://www.github.com/steventen/sql_tracker'
15
+ spec.license = 'MIT'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = 'bin'
19
+ spec.executables = ['sql_tracker']
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.add_development_dependency 'bundler', '~> 1.12'
23
+ spec.add_development_dependency 'rake', '~> 10.0'
24
+ spec.add_development_dependency 'minitest', '~> 5.0'
25
+ spec.add_development_dependency 'activesupport', '>= 3.0.0'
26
+ end
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sql_tracker
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Steven Yue
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-09-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.12'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.12'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: activesupport
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 3.0.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 3.0.0
69
+ description: Track and analyze sql queries of your rails application
70
+ email:
71
+ - jincheker@gmail.com
72
+ executables:
73
+ - sql_tracker
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".gitignore"
78
+ - ".travis.yml"
79
+ - Gemfile
80
+ - LICENSE.txt
81
+ - README.md
82
+ - Rakefile
83
+ - bin/console
84
+ - bin/setup
85
+ - bin/sql_tracker
86
+ - lib/sql_tracker.rb
87
+ - lib/sql_tracker/config.rb
88
+ - lib/sql_tracker/handler.rb
89
+ - lib/sql_tracker/railtie.rb
90
+ - lib/sql_tracker/report.rb
91
+ - lib/sql_tracker/version.rb
92
+ - sql_tracker.gemspec
93
+ homepage: http://www.github.com/steventen/sql_tracker
94
+ licenses:
95
+ - MIT
96
+ metadata: {}
97
+ post_install_message:
98
+ rdoc_options: []
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ requirements: []
112
+ rubyforge_project:
113
+ rubygems_version: 2.5.1
114
+ signing_key:
115
+ specification_version: 4
116
+ summary: Rails SQL Query Tracker
117
+ test_files: []