mongo_profiler 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +4 -0
  5. data/Gemfile.lock +78 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +112 -0
  8. data/Rakefile +1 -0
  9. data/assets/mongo_profiler_dashboard_index.png +0 -0
  10. data/assets/mongo_profiler_group_details.png +0 -0
  11. data/assets/mongo_profiler_query_details.png +0 -0
  12. data/assets/mongo_profiler_query_details_backtrace.png +0 -0
  13. data/config.ru +13 -0
  14. data/lib/mongo_profiler.rb +108 -0
  15. data/lib/mongo_profiler/caller.rb +27 -0
  16. data/lib/mongo_profiler/extensions/mongo/cursor.rb +38 -0
  17. data/lib/mongo_profiler/payload.rb +33 -0
  18. data/lib/mongo_profiler/profiler.rb +4 -0
  19. data/lib/mongo_profiler/stats.rb +25 -0
  20. data/lib/mongo_profiler/version.rb +3 -0
  21. data/lib/mongo_profiler/web.rb +85 -0
  22. data/lib/mongo_profiler/web_helpers.rb +65 -0
  23. data/mongo_profiler.gemspec +31 -0
  24. data/spec/mongo_profiler/caller_spec.rb +83 -0
  25. data/spec/mongo_profiler/extensions/mongo/cursor_spec.rb +42 -0
  26. data/spec/mongo_profiler/payload_spec.rb +111 -0
  27. data/spec/mongo_profiler/profiler_spec.rb +8 -0
  28. data/spec/mongo_profiler/stats_spec.rb +29 -0
  29. data/spec/mongo_profiler/web_helpers_spec.rb +52 -0
  30. data/spec/mongo_profiler/web_spec.rb +43 -0
  31. data/spec/mongo_profiler_spec.rb +113 -0
  32. data/spec/spec_helper.rb +28 -0
  33. data/web/assets/fonts/glyphicons-halflings-regular.eot +0 -0
  34. data/web/assets/fonts/glyphicons-halflings-regular.svg +229 -0
  35. data/web/assets/fonts/glyphicons-halflings-regular.ttf +0 -0
  36. data/web/assets/fonts/glyphicons-halflings-regular.woff +0 -0
  37. data/web/assets/javascripts/bootstrap.js +1951 -0
  38. data/web/assets/javascripts/bootstrap.min.js +6 -0
  39. data/web/assets/javascripts/highlight.pack.js +1 -0
  40. data/web/assets/javascripts/jquery.js +10337 -0
  41. data/web/assets/stylesheets/application.css +10 -0
  42. data/web/assets/stylesheets/bootstrap.css +5831 -0
  43. data/web/assets/stylesheets/bootstrap.min.css +7 -0
  44. data/web/assets/stylesheets/highlight/default.css +153 -0
  45. data/web/assets/stylesheets/highlight/github.css +125 -0
  46. data/web/views/group_id.erb +53 -0
  47. data/web/views/index.erb +32 -0
  48. data/web/views/layout.erb +94 -0
  49. data/web/views/show.erb +83 -0
  50. metadata +227 -0
@@ -0,0 +1,4 @@
1
+ module MongoProfiler
2
+ class Profiler
3
+ end
4
+ end
@@ -0,0 +1,25 @@
1
+ module MongoProfiler
2
+ class Stats
3
+ def initialize(stats_client)
4
+ @stats_client = stats_client
5
+ end
6
+
7
+ def populate(_caller, total_time)
8
+ file = sanitaze_stat_key _caller.file.split('/').last
9
+ method = sanitaze_stat_key _caller.method
10
+
11
+ stat_name = "mongo_profiler.#{MongoProfiler.application_name}.#{file}.#{method}"
12
+
13
+ total_time_ms = total_time * 1000
14
+
15
+ @stats_client.increment stat_name
16
+ @stats_client.timing stat_name, total_time_ms
17
+ end
18
+
19
+ private
20
+
21
+ def sanitaze_stat_key(key)
22
+ key.gsub(/\W/, '_')
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,3 @@
1
+ module MongoProfiler
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,85 @@
1
+ # https://github.com/mperham/sidekiq/wiki/Monitoring
2
+ require 'erb'
3
+ require 'yaml'
4
+ require 'sinatra/base'
5
+ require 'json'
6
+
7
+ require 'mongo_profiler/web_helpers'
8
+
9
+ module MongoProfiler
10
+ class Web < Sinatra::Base
11
+
12
+ set :root, File.expand_path(File.dirname(__FILE__) + "/../../web")
13
+ set :public_folder, Proc.new { "#{root}/assets" }
14
+ set :views, Proc.new { "#{root}/views" }
15
+
16
+ helpers WebHelpers
17
+
18
+
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 } })
24
+
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
29
+ end
30
+
31
+ post '/profiler/enable' do
32
+ MongoProfiler.enable!
33
+
34
+ redirect to('/')
35
+ end
36
+
37
+ post '/profiler/disable' do
38
+ MongoProfiler.disable!
39
+
40
+ redirect to('/')
41
+ 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
+ end
85
+ end
@@ -0,0 +1,65 @@
1
+ require 'uri'
2
+
3
+ module MongoProfiler
4
+ module WebHelpers
5
+ def root_path
6
+ "#{env['SCRIPT_NAME']}/"
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
+ end
65
+ end
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mongo_profiler/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "mongo_profiler"
8
+ spec.version = MongoProfiler::VERSION
9
+ spec.authors = ["Pablo Cantero"]
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}
13
+ spec.homepage = "https://github.com/phstc/mongo_profiler"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "activesupport"
22
+
23
+ spec.add_development_dependency "rake"
24
+ spec.add_development_dependency "mongo", "1.9.2"
25
+ spec.add_development_dependency "bson_ext"
26
+ spec.add_development_dependency "rspec", "~> 2.14.1"
27
+ spec.add_development_dependency "pry-byebug"
28
+ spec.add_development_dependency "sinatra"
29
+ spec.add_development_dependency "shotgun"
30
+ spec.add_development_dependency "rack-test"
31
+ end
@@ -0,0 +1,83 @@
1
+ require 'spec_helper'
2
+
3
+ module MongoProfiler
4
+ describe Caller do
5
+ subject { described_class.new(_caller) }
6
+
7
+ let(:_caller) {
8
+ [
9
+ "/Users/pablo/workspace/project/spec/mongo_profiler_spec.rb:7:in `new'",
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
+ "/Users/pablo/.gem/ruby/2.0.0/gems/rspec-core-2.14.4/lib/rspec/core/memoized_helpers.rb:199:in `fetch'",
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
+ ]
14
+ }
15
+
16
+ its(:file) { should end_with('project/spec/mongo_profiler_spec.rb') }
17
+ its(:line) { should eq 7 }
18
+ its(:method) { should eq 'new' }
19
+ its(:_caller) { should eq _caller }
20
+
21
+ context 'when stacktrace starts with bundle or gem' do
22
+ let(:_caller) {
23
+ [
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
+ "/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'",
27
+ "/Users/pablo/.gem/ruby/2.0.0/gems/rspec-core-2.14.4/lib/rspec/core/memoized_helpers.rb:199:in `fetch'",
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
+ ]
30
+ }
31
+
32
+ its(:file) { should end_with('project/spec/mongo_profiler_spec.rb') }
33
+ its(:line) { should eq 7 }
34
+ its(:method) { should eq 'new' }
35
+ its(:_caller) { should eq _caller }
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
+ end
83
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Mongo::Cursor' do
4
+ let(:collection) { COLL }
5
+
6
+ describe '#send_initial_query' do
7
+ context 'when enabled' do
8
+ let(:stats_client) { double('StatsD client').as_null_object }
9
+
10
+ before do
11
+ MongoProfiler.enable!
12
+
13
+ MongoProfiler.stats_client = stats_client
14
+ end
15
+
16
+ it 'populate stats' do
17
+ expect(stats_client).to receive(:increment).with(any_args)
18
+ expect(stats_client).to receive(:timing).with(any_args)
19
+
20
+ collection.find_one
21
+ end
22
+
23
+ it 'creates a profiler entry' do
24
+ expect {
25
+ collection.insert(test: 'test')
26
+ expect(collection.find_one['test']).to eq 'test'
27
+ }.to change { MongoProfiler.collection.count }.by(1)
28
+ end
29
+ end
30
+
31
+ context 'when disabled' do
32
+ before { MongoProfiler.disable! }
33
+
34
+ it 'skips profiler entry' do
35
+ expect {
36
+ collection.insert(test: 'test')
37
+ expect(collection.find_one['test']).to eq 'test'
38
+ }.to_not change { MongoProfiler.collection.count }
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,111 @@
1
+ require 'spec_helper'
2
+
3
+ module MongoProfiler
4
+ describe Payload do
5
+ subject { described_class.new(payload) }
6
+
7
+ describe '#system_database?' do
8
+ context 'when admin' do
9
+ let(:payload) { { database: 'admin' } }
10
+
11
+ its(:system_database?) { should be_true }
12
+ end
13
+
14
+ context 'when system' do
15
+ let(:payload) { { database: 'system' } }
16
+
17
+ its(:system_database?) { should be_true }
18
+ end
19
+
20
+ context 'when other' do
21
+ let(:payload) { { database: 'other' } }
22
+
23
+ its(:system_database?) { should be_false }
24
+ end
25
+ end
26
+
27
+ describe '#system_collection?' do
28
+ context 'when admin' do
29
+ let(:payload) { { collection: 'mongo_profiler' } }
30
+
31
+ its(:system_collection?) { should be_true }
32
+ end
33
+
34
+ context 'when system' do
35
+ let(:payload) { { collection: 'system' } }
36
+
37
+ its(:system_collection?) { should be_true }
38
+ end
39
+
40
+ context 'when other' do
41
+ let(:payload) { { collection: 'other' } }
42
+
43
+ its(:system_collection?) { should be_false }
44
+ end
45
+ end
46
+
47
+ describe '#system_count?' do
48
+ context 'when admin' do
49
+ let(:payload) { { selector: { count: 'mongo_profiler' } } }
50
+
51
+ its(:system_count?) { should be_true }
52
+ end
53
+
54
+ context 'when system' do
55
+ let(:payload) { { selector: { count: 'system' } } }
56
+
57
+ its(:system_count?) { should be_true }
58
+ end
59
+
60
+ context 'when other' do
61
+ let(:payload) { { selector: { count: 'other' } } }
62
+
63
+ its(:system_count?) { should be_false }
64
+ end
65
+ end
66
+
67
+ describe '#system_distinct?' do
68
+ context 'when admin' do
69
+ let(:payload) { { selector: { distinct: 'mongo_profiler' } } }
70
+
71
+ its(:system_distinct?) { should be_true }
72
+ end
73
+
74
+ context 'when system' do
75
+ let(:payload) { { selector: { distinct: 'system' } } }
76
+
77
+ its(:system_distinct?) { should be_true }
78
+ end
79
+
80
+ context 'when other' do
81
+ let(:payload) { { selector: { distinct: 'other' } } }
82
+
83
+ its(:system_distinct?) { should be_false }
84
+ end
85
+ end
86
+
87
+ describe '#system_command?' do
88
+ context 'when count' do
89
+ let(:payload) { { collection: '$cmd', selector: { count: 'something' } } }
90
+
91
+ its(:system_command?) { should be_false }
92
+ end
93
+
94
+ context 'when distinct' do
95
+ let(:payload) { { collection: '$cmd', selector: { distinct: 'something' } } }
96
+
97
+ its(:system_command?) { should be_false }
98
+ end
99
+
100
+ context 'when other' do
101
+ let(:payload) { { collection: '$cmd', selector: { getnonce: -1 } } }
102
+
103
+ its(:system_command?) { should be_true }
104
+ end
105
+ end
106
+
107
+ describe '#system_any?' do
108
+ pending
109
+ end
110
+ end
111
+ end