mongo_profiler 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ Moped::Node.class_eval do
2
+ alias_method :original_query, :query
3
+
4
+ def query(database, collection, selector, options = {})
5
+ started_at = Time.now
6
+
7
+ result = original_query(database, collection, selector, options)
8
+
9
+ begin
10
+ MongoProfiler::Profile.register(started_at, database, collection, selector, options)
11
+ rescue => e
12
+ p "MongoProfiler: #{e.message}"
13
+ end
14
+
15
+ result
16
+ end
17
+ end
@@ -0,0 +1,107 @@
1
+ require 'digest'
2
+
3
+ module MongoProfiler
4
+ class Profile
5
+ include Mongoid::Document
6
+ include Mongoid::Timestamps
7
+
8
+ field :total_time, type: Float
9
+ field :command_database, type: String
10
+ field :command_collection, type: String
11
+ field :command, type: String
12
+ field :explain, type: String
13
+ field :file, type: String
14
+ field :line, type: Integer
15
+ field :method, type: String
16
+ field :profile_md5, type: String
17
+
18
+ belongs_to :profile_group
19
+
20
+ index group_id: 1, profile_md5: 1
21
+
22
+ def score
23
+ explain = JSON.parse(self.explain)
24
+
25
+ n = explain['n']
26
+ ns_scanned = explain['nscanned']
27
+ cursor = explain['cursor']
28
+ scan_and_order = explain['scanAndOrder']
29
+
30
+ case
31
+ when cursor == 'BasicCursor'
32
+ :no_index
33
+ when n == 0
34
+ :no_docs_found
35
+ when ns_scanned == n
36
+ :perfect
37
+ when ns_scanned > n
38
+ :scanned_more_than_returned
39
+ when scan_and_order
40
+ :had_to_order
41
+ end
42
+ rescue => e
43
+ e.message
44
+ end
45
+
46
+ class << self
47
+ def register(started_at, database, collection, selector, options = {})
48
+ return if collection =~ /mongo_profiler/ || collection =~ /system/
49
+ return if selector['$explain']
50
+
51
+ _caller = MongoProfiler::Caller.new(caller)
52
+
53
+ group = ProfileGroup.find_or_create_by(name: MongoProfiler.current_group_name)
54
+
55
+ group.touch
56
+
57
+ profile_md5 = generate_profile_md5(database, collection, selector, _caller)
58
+
59
+ return if Profile.where(profile_md5: profile_md5, profile_group_id: group.id).any?
60
+
61
+ result = {}
62
+ result[:profile_md5] = profile_md5
63
+ result[:profile_group_id] = group.id
64
+
65
+ result[:total_time] = elapsed(started_at)
66
+ result[:command_database] = database
67
+ result[:command_collection] = collection
68
+ result[:command] = JSON.dump(selector)
69
+ result[:file] = _caller.file
70
+ result[:line] = _caller.line
71
+ result[:method] = _caller.method
72
+
73
+ # TODO do it in background
74
+ result[:explain] = JSON.dump(generate_explain(collection, selector))
75
+
76
+ self.create(result)
77
+ end
78
+
79
+ private
80
+
81
+ def generate_explain(collection, selector)
82
+ query = if selector.has_key?('$query')
83
+ selector['$query']
84
+ else
85
+ selector
86
+ end
87
+ self.collection.database[collection].find(query).explain
88
+ end
89
+
90
+ def generate_profile_md5(database, collection, selector, _caller)
91
+ profile_key = [
92
+ database,
93
+ collection,
94
+ MongoProfiler::Util.deep_keys(selector).join,
95
+ _caller.file,
96
+ _caller.line.to_s
97
+ ].join
98
+
99
+ Digest::MD5.hexdigest(profile_key)
100
+ end
101
+
102
+ def elapsed(started_at)
103
+ (Time.now - started_at) * 1000
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,39 @@
1
+ module MongoProfiler
2
+ class ProfileGroup
3
+ include Mongoid::Document
4
+ include Mongoid::Timestamps
5
+
6
+ field :name, type: String
7
+
8
+ has_many :profiles, dependent: :delete
9
+
10
+ index name: 1
11
+
12
+ def total_time
13
+ profiles.sum(&:total_time)
14
+ end
15
+
16
+ def avg_time
17
+ return 0 if (count = profiles.count) == 0
18
+ total_time / count
19
+ end
20
+
21
+ def min_time
22
+ profiles.collect(&:total_time).min
23
+ end
24
+
25
+ def max_time
26
+ profiles.collect(&:total_time).max
27
+ end
28
+
29
+ def filter_by_score(score)
30
+ profiles.select do |p|
31
+ p.score == score
32
+ end
33
+ end
34
+
35
+ def count_by_score(score)
36
+ filter_by_score(score).size
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,21 @@
1
+ module MongoProfiler
2
+ class Util
3
+ class << self
4
+ def deep_keys(hash)
5
+ return [] unless hash.is_a? Hash
6
+
7
+ hash.inject([]) do |keys, (key, value)|
8
+ keys << key
9
+ case value
10
+ when Hash
11
+ keys.concat deep_keys(value)
12
+ when Array
13
+ value.each { |vvalue| keys.concat deep_keys(vvalue) }
14
+ end
15
+
16
+ keys
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,3 +1,3 @@
1
1
  module MongoProfiler
2
- VERSION = '0.0.1'
2
+ VERSION = '0.0.2'
3
3
  end
@@ -17,69 +17,22 @@ module MongoProfiler
17
17
 
18
18
 
19
19
  get '/' do
20
- @profiles = MongoProfiler.collection.
21
- group({ key: %i[method file application_name group_id],
22
- reduce: 'function(curr, result) { result.total_time += curr.total_time; result.total += 1 }',
23
- initial: { total: 0, total_time: 0 } })
20
+ @groups = MongoProfiler::ProfileGroup.order(:updated_at.desc)
24
21
 
25
- # group profilers by group_id and sort by created_at DESC
26
- @grouped_profiles = @profiles.group_by { |profile| profile['group_id'] }.to_a.reverse
27
-
28
- erb :index
22
+ erb :index
29
23
  end
30
24
 
31
- post '/profiler/enable' do
32
- MongoProfiler.enable!
25
+ get '/groups/:id' do
26
+ @group = MongoProfiler::ProfileGroup.find(params[:id])
33
27
 
34
- redirect to('/')
28
+ erb :show
35
29
  end
36
30
 
37
- post '/profiler/disable' do
38
- MongoProfiler.disable!
31
+ post '/clear' do
32
+ MongoProfiler::ProfileGroup.delete_all
33
+ MongoProfiler::Profile.delete_all
39
34
 
40
35
  redirect to('/')
41
36
  end
42
-
43
- get '/profiler/groups/:group_id' do
44
- @group_id = params[:group_id]
45
- @profiles = MongoProfiler.collection.find(group_id: @group_id).to_a
46
-
47
- @sample_profile = @profiles.first
48
-
49
- @profiles_count = @profiles.count
50
- @profiles_total_time = @profiles.reduce(0) { |sum, p| sum + p['total_time'] }
51
-
52
- @grouped_profiles = @profiles.group_by { |profile| profile['method'] }
53
-
54
- erb :group_id
55
- end
56
-
57
- get '/profiler/:_id' do
58
- @profile_id = BSON::ObjectId(params[:_id])
59
- @profile = MongoProfiler.collection.find_one(_id: @profile_id)
60
- @instrument_payload = JSON.parse(@profile['instrument_payload'])
61
-
62
- @collection_name = @instrument_payload['collection']
63
- @selector = @instrument_payload['selector']
64
-
65
- begin
66
- @selector.each_pair do |key, value|
67
- if value.is_a? Hash
68
- if value.first[0] == '$oid'
69
- @selector[key] = BSON::ObjectId(value.first[1])
70
- end
71
- end
72
- end
73
- @explain = MongoProfiler.database[@collection_name].find(@selector).explain
74
- rescue => e
75
- @explain = { error: "Unable to generate explain: #{e.message}" }
76
- end
77
-
78
- # http://docs.mongodb.org/manual/core/capped-collections/
79
- # You can update documents in a collection after inserting them. However, these updates cannot cause the documents to grow. If the update operation causes the document to grow beyond their original size, the update operation will fail.
80
- # If you plan to update documents in a capped collection, create an index so that these update operations do not require a table scan.
81
- # MongoRubyProfiler.collection.update({ _id: BSON::ObjectId(params[:_id]) }, '$set' => { explain: explain } )
82
- erb :show
83
- end
84
37
  end
85
38
  end
@@ -5,61 +5,5 @@ module MongoProfiler
5
5
  def root_path
6
6
  "#{env['SCRIPT_NAME']}/"
7
7
  end
8
-
9
- def graphite_graph_timers_url(profile, from, size, title)
10
- file = profile['file']
11
- method = profile['method']
12
- application_name = profile['application_name']
13
-
14
- URI.escape(["#{MongoProfiler.graphite_url}/render?",
15
- "from=#{from}&",
16
- 'until=now&width=400&height=250&',
17
- "target=#{graphite_timers_target(application_name, file, method, size)}",
18
- "&title=#{title}"].join)
19
- end
20
-
21
- def graphite_graph_count_url(profile, from, size, title)
22
- file = profile['file']
23
- method = profile['method']
24
- application_name = profile['application_name']
25
-
26
- URI.escape(["#{MongoProfiler.graphite_url}/render?",
27
- "from=#{from}&",
28
- 'until=now&width=400&height=250&',
29
- "target=#{graphite_count_target(application_name, file, method, size)}",
30
- "&title=#{title}"].join)
31
- end
32
-
33
- def print_backtrace_entry(entry)
34
- if entry.include?('gem/ruby') || entry.include?('rubies/ruby') || entry.include?('bundle/ruby')
35
- entry
36
- else
37
- %{<span class="btn-info">#{entry}</span>}
38
- end
39
- end
40
-
41
- private
42
-
43
- def graphite_timers_target(application_name, file, method, size)
44
- file = file.split('/').last
45
- file_key = sanitaze_stat_key(file)
46
- method_key = sanitaze_stat_key(method)
47
-
48
- "alias(summarize(stats.timers.#{MongoProfiler.stats_prefix}mongo_profiler.#{application_name}.#{file_key}.#{method_key}.mean, '#{size}', 'mean'), '#{file}##{method}')"
49
- end
50
-
51
- def graphite_count_target(application_name, file, method, size)
52
- file = file.split('/').last
53
- file_key = sanitaze_stat_key(file)
54
- method_key = sanitaze_stat_key(method)
55
-
56
- binding.pry
57
-
58
- "alias(summarize(stats_counts.#{MongoProfiler.stats_prefix}mongo_profiler.#{application_name}.#{file_key}.#{method_key}, '#{size}', 'sum'), '#{file}##{method}')"
59
- end
60
-
61
- def sanitaze_stat_key(key)
62
- key.gsub(/\W/, '_')
63
- end
64
8
  end
65
9
  end
@@ -8,8 +8,7 @@ Gem::Specification.new do |spec|
8
8
  spec.version = MongoProfiler::VERSION
9
9
  spec.authors = ["Pablo Cantero"]
10
10
  spec.email = ["pablo@pablocantero.com"]
11
- spec.summary = %q{Ruby profiling tool for MongoDB}
12
- spec.description = %q{A Ruby profiling tool for MongoDB}
11
+ spec.summary = spec.description = %q{Ruby profiling tool for MongoDB}
13
12
  spec.homepage = "https://github.com/phstc/mongo_profiler"
14
13
  spec.license = "MIT"
15
14
 
@@ -21,11 +20,11 @@ Gem::Specification.new do |spec|
21
20
  spec.add_dependency "activesupport"
22
21
 
23
22
  spec.add_development_dependency "rake"
24
- spec.add_development_dependency "mongo", "1.9.2"
25
- spec.add_development_dependency "bson_ext"
23
+ spec.add_development_dependency "mongoid"
26
24
  spec.add_development_dependency "rspec", "~> 2.14.1"
27
25
  spec.add_development_dependency "pry-byebug"
28
26
  spec.add_development_dependency "sinatra"
29
27
  spec.add_development_dependency "shotgun"
30
28
  spec.add_development_dependency "rack-test"
29
+ spec.add_development_dependency "database_cleaner"
31
30
  end
@@ -6,78 +6,33 @@ module MongoProfiler
6
6
 
7
7
  let(:_caller) {
8
8
  [
9
- "/Users/pablo/workspace/project/spec/mongo_profiler_spec.rb:7:in `new'",
9
+ "/Users/pablo/workspace/project/test.rb:7:in `new'",
10
10
  "/Users/pablo/.gem/ruby/2.0.0/gems/rspec-core-2.14.4/lib/rspec/core/memoized_helpers.rb:199:in `block (2 levels) in let'",
11
11
  "/Users/pablo/.gem/ruby/2.0.0/gems/rspec-core-2.14.4/lib/rspec/core/memoized_helpers.rb:199:in `fetch'",
12
12
  "/Users/pablo/.gem/ruby/2.0.0/gems/rspec-core-2.14.4/lib/rspec/core/memoized_helpers.rb:199:in `block in let'"
13
13
  ]
14
14
  }
15
15
 
16
- its(:file) { should end_with('project/spec/mongo_profiler_spec.rb') }
16
+ its(:file) { should end_with('project/test.rb') }
17
17
  its(:line) { should eq 7 }
18
18
  its(:method) { should eq 'new' }
19
19
  its(:_caller) { should eq _caller }
20
20
 
21
- context 'when stacktrace starts with bundle or gem' do
21
+ context 'when backtrace starts with bundle or gem' do
22
22
  let(:_caller) {
23
23
  [
24
24
  "/Users/pablo/.gem/ruby/2.0.0/gems/rspec-core-2.14.4/lib/rspec/core/memoized_helpers.rb:199:in `block (2 levels) in let'",
25
25
  "/Users/pablo/bundle/ruby/2.0.0/gems/rspec-core-2.14.4/lib/rspec/core/memoized_helpers.rb:199:in `block (2 levels) in let'",
26
- "/Users/pablo/workspace/project/spec/mongo_profiler_spec.rb:7:in `new'",
26
+ "/Users/pablo/workspace/project/test.rb:7:in `new'",
27
27
  "/Users/pablo/.gem/ruby/2.0.0/gems/rspec-core-2.14.4/lib/rspec/core/memoized_helpers.rb:199:in `fetch'",
28
28
  "/Users/pablo/.gem/ruby/2.0.0/gems/rspec-core-2.14.4/lib/rspec/core/memoized_helpers.rb:199:in `block in let'"
29
29
  ]
30
30
  }
31
31
 
32
- its(:file) { should end_with('project/spec/mongo_profiler_spec.rb') }
32
+ its(:file) { should end_with('project/test.rb') }
33
33
  its(:line) { should eq 7 }
34
34
  its(:method) { should eq 'new' }
35
35
  its(:_caller) { should eq _caller }
36
36
  end
37
-
38
- describe '#mongo_profiler_caller' do
39
-
40
- context 'when mongo_profiler' do
41
- let(:_caller) {
42
- [
43
- "/Users/pablo/.gem/ruby/2.0.0/gems/rspec-core-2.14.4/lib/rspec/core/memoized_helpers.rb:199:in `block (2 levels) in let'",
44
- "/Users/pablo/bundle/ruby/2.0.0/gems/rspec-core-2.14.4/lib/rspec/core/memoized_helpers.rb:199:in `block (2 levels) in let'",
45
- "/Users/pablo/workspace/project/spec/mongo_profiler.rb:7:in `new'",
46
- "/Users/pablo/.gem/ruby/2.0.0/gems/rspec-core-2.14.4/lib/rspec/core/memoized_helpers.rb:199:in `fetch'",
47
- "/Users/pablo/.gem/ruby/2.0.0/gems/rspec-core-2.14.4/lib/rspec/core/memoized_helpers.rb:199:in `block in let'"
48
- ]
49
- }
50
-
51
- its(:mongo_profiler_caller?) { should be_true }
52
-
53
- context 'when _spec' do
54
- let(:_caller) {
55
- [
56
- "/Users/pablo/.gem/ruby/2.0.0/gems/rspec-core-2.14.4/lib/rspec/core/memoized_helpers.rb:199:in `block (2 levels) in let'",
57
- "/Users/pablo/bundle/ruby/2.0.0/gems/rspec-core-2.14.4/lib/rspec/core/memoized_helpers.rb:199:in `block (2 levels) in let'",
58
- "/Users/pablo/workspace/project/spec/mongo_profiler_spec.rb:7:in `new'",
59
- "/Users/pablo/.gem/ruby/2.0.0/gems/rspec-core-2.14.4/lib/rspec/core/memoized_helpers.rb:199:in `fetch'",
60
- "/Users/pablo/.gem/ruby/2.0.0/gems/rspec-core-2.14.4/lib/rspec/core/memoized_helpers.rb:199:in `block in let'"
61
- ]
62
- }
63
-
64
- its(:mongo_profiler_caller?) { should be_false }
65
- end
66
- end
67
-
68
- context 'when external' do
69
- let(:_caller) {
70
- [
71
- "/Users/pablo/.gem/ruby/2.0.0/gems/rspec-core-2.14.4/lib/rspec/core/memoized_helpers.rb:199:in `block (2 levels) in let'",
72
- "/Users/pablo/bundle/ruby/2.0.0/gems/rspec-core-2.14.4/lib/rspec/core/memoized_helpers.rb:199:in `block (2 levels) in let'",
73
- "/Users/pablo/workspace/project/file.rb:7:in `new'",
74
- "/Users/pablo/.gem/ruby/2.0.0/gems/rspec-core-2.14.4/lib/rspec/core/memoized_helpers.rb:199:in `fetch'",
75
- "/Users/pablo/.gem/ruby/2.0.0/gems/rspec-core-2.14.4/lib/rspec/core/memoized_helpers.rb:199:in `block in let'"
76
- ]
77
- }
78
-
79
- its(:mongo_profiler_caller?) { should be_false }
80
- end
81
- end
82
37
  end
83
38
  end
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+
3
+ module MongoProfiler
4
+ describe Caller do
5
+ it 'creates a profile' do
6
+ test = TestModel.create(name: 'Pablo')
7
+
8
+ expect(TestModel.where(name: 'Pablo').first.name).to eq 'Pablo'
9
+
10
+ expect(MongoProfiler::ProfileGroup.count).to eq 1
11
+
12
+ group = MongoProfiler::ProfileGroup.first
13
+ expect(group.name).to eq 'Undefined group name'
14
+
15
+ expect(MongoProfiler::Profile.count).to eq 1
16
+
17
+ profile = MongoProfiler::Profile.first
18
+
19
+ expect(profile.attributes).to include('profile_group_id' => group.id,
20
+ 'file' => __FILE__,
21
+ 'command_database' => 'mongo_profiler_test',
22
+ 'command_collection' => 'test_models')
23
+
24
+ expect(JSON.parse(profile.command)).to eq('$query' => { 'name' => 'Pablo' }, '$orderby' => { '_id' => 1 })
25
+ end
26
+
27
+ it 'does not duplicate profiles' do
28
+ test = TestModel.create
29
+
30
+ # To guarantee the same line number
31
+ TestModel.where(name: 'Pablo').first || TestModel.where(name: 'Pablo').first
32
+
33
+ expect(MongoProfiler::ProfileGroup.count).to eq 1
34
+ expect(MongoProfiler::Profile.count).to eq 1
35
+ end
36
+
37
+ it 'creates a new profile if the query keys change' do
38
+ test = TestModel.create
39
+
40
+ TestModel.where(name: 'Pablo').first
41
+ TestModel.where(last_name: 'Cantero').first
42
+
43
+ expect(MongoProfiler::ProfileGroup.count).to eq 1
44
+ expect(MongoProfiler::Profile.count).to eq 2
45
+ end
46
+
47
+ it 'uses supplied group name' do
48
+ MongoProfiler.current_group_name = 'Test'
49
+
50
+ test = TestModel.create(name: 'Pablo')
51
+
52
+ TestModel.where(name: 'Pablo').first
53
+
54
+ expect(MongoProfiler::ProfileGroup.count).to eq 1
55
+
56
+ group = MongoProfiler::ProfileGroup.first
57
+ expect(group.name).to eq 'Test'
58
+ end
59
+ end
60
+ end