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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a47fc67c8b76da256d794a635b3e719f783664aa
4
+ data.tar.gz: 19b951a31adeebadf0e38de08b2768bac0111e58
5
+ SHA512:
6
+ metadata.gz: 8835e0823b0f5b7b008dc714178a3e4412bcc36f6d00eb2eb4b49f1b71fa70f143bc2f261e88bb8b8a9e66cfc5db0514d7c752f6d5407daa5064d1f5080a99f3
7
+ data.tar.gz: 4889bf3ffe0465f163b0a13d977747956d2df0c61f3ece6ff38cabdcedef770195768350f7140147209e7c4eed2c262b9a672f3ec27a3e112c8a1e7fbc51a260
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ web/assets/bower_components
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format d
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in mongo-ruby-profiler.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,78 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ mongo_profiler (0.0.1)
5
+ activesupport
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activesupport (4.0.2)
11
+ i18n (~> 0.6, >= 0.6.4)
12
+ minitest (~> 4.2)
13
+ multi_json (~> 1.3)
14
+ thread_safe (~> 0.1)
15
+ tzinfo (~> 0.3.37)
16
+ atomic (1.1.14)
17
+ bson (1.9.2)
18
+ bson_ext (1.9.2)
19
+ bson (~> 1.9.2)
20
+ byebug (2.5.0)
21
+ columnize (~> 0.3.6)
22
+ debugger-linecache (~> 1.2.0)
23
+ coderay (1.1.0)
24
+ columnize (0.3.6)
25
+ debugger-linecache (1.2.0)
26
+ diff-lcs (1.2.5)
27
+ i18n (0.6.9)
28
+ method_source (0.8.2)
29
+ minitest (4.7.5)
30
+ mongo (1.9.2)
31
+ bson (~> 1.9.2)
32
+ multi_json (1.8.4)
33
+ pry (0.9.12.4)
34
+ coderay (~> 1.0)
35
+ method_source (~> 0.8)
36
+ slop (~> 3.4)
37
+ pry-byebug (1.2.1)
38
+ byebug (~> 2.2)
39
+ pry (~> 0.9.12)
40
+ rack (1.5.2)
41
+ rack-protection (1.5.1)
42
+ rack
43
+ rack-test (0.6.2)
44
+ rack (>= 1.0)
45
+ rake (10.1.1)
46
+ rspec (2.14.1)
47
+ rspec-core (~> 2.14.0)
48
+ rspec-expectations (~> 2.14.0)
49
+ rspec-mocks (~> 2.14.0)
50
+ rspec-core (2.14.7)
51
+ rspec-expectations (2.14.4)
52
+ diff-lcs (>= 1.1.3, < 2.0)
53
+ rspec-mocks (2.14.4)
54
+ shotgun (0.9)
55
+ rack (>= 1.0)
56
+ sinatra (1.4.4)
57
+ rack (~> 1.4)
58
+ rack-protection (~> 1.4)
59
+ tilt (~> 1.3, >= 1.3.4)
60
+ slop (3.4.7)
61
+ thread_safe (0.1.3)
62
+ atomic
63
+ tilt (1.4.1)
64
+ tzinfo (0.3.38)
65
+
66
+ PLATFORMS
67
+ ruby
68
+
69
+ DEPENDENCIES
70
+ bson_ext
71
+ mongo (= 1.9.2)
72
+ mongo_profiler!
73
+ pry-byebug
74
+ rack-test
75
+ rake
76
+ rspec (~> 2.14.1)
77
+ shotgun
78
+ sinatra
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Pablo Cantero
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # Mongo Profiler
2
+
3
+ Database profiling tools are awesome and always useful. I love [Mongo Profiling](http://docs.mongodb.org/manual/tutorial/manage-the-database-profiler/). But unfortunately these tools don't match queries and code they are profiling, so sometimes isn't easy to match where the slow queries are performed.
4
+
5
+ The Mongo Profiler is a <del>refinement</del> patch in the [mongo-ruby-driver](https://github.com/mongodb/mongo-ruby-driver) to log all execute queries and their respective callers in a [capped collections](http://docs.mongodb.org/manual/core/capped-collections/).
6
+
7
+ It isn't competitor for the Mongo's built-in profiling, it is just a complementary tool to help us to profile our queries.
8
+
9
+ An interesting feature in the Mongo Profiler is that we can group queries by "life cycles". For example, in a web application it can be the `request_id`, so you will be able to see how many queries, how long did they take, the explain plans etc for a specific request.
10
+
11
+ First time I used it, I was shocked to see some pages doing lot of duplicated queries, even though some were really fast, they were unnecessary, I could get rid of some of them just by "memorising" some documents.
12
+
13
+ ## Installation
14
+
15
+ Add this line to your application's Gemfile:
16
+
17
+ gem 'mongo_profiler'
18
+
19
+ And then execute:
20
+
21
+ $ bundle
22
+
23
+ Or install it yourself as:
24
+
25
+ $ gem install mongo_profiler
26
+
27
+ ## Usage
28
+
29
+ ### Rails application
30
+
31
+ ```ruby
32
+ # config/initializers/mongo_profiler_setup.rb
33
+
34
+ require 'mongo_profiler'
35
+ require 'mongo_profiler/extensions/mongo/cursor'
36
+
37
+ MongoProfiler.connect('localhost', 27017, 'my_database')
38
+
39
+ MongoProfiler.application_name = 'my_application'
40
+
41
+ # To enable Statsd
42
+ # MongoProfiler.stats_client = MyStatsdClientInstance
43
+
44
+ # To show graphite graphs
45
+ # MongoProfiler.graphite_url = 'http://my_graphite'
46
+ ```
47
+
48
+ ```ruby
49
+ # app/controllers/application_controller.rb
50
+
51
+ class ApplicationController < ActionController::Base
52
+ before_filter :mongo_profiler_setup
53
+
54
+ private
55
+
56
+ def mongo_profiler_setup
57
+ # aggregate queries by request
58
+ MongoProfiler.group_id = "request-#{request.uuid}"
59
+
60
+ # to show the request url
61
+ MongoProfiler.extra_attrs[:request_url] = request.url
62
+ rescue => e
63
+ p "MongoProfiler: #{e.message}"
64
+ end
65
+ end
66
+ ```
67
+
68
+ ```ruby
69
+ # config/routes.rb
70
+
71
+ require 'mongo_profiler/web'
72
+
73
+ MyApplication::Application.routes.draw do
74
+ mount MongoProfiler::Web => '/mongo_profiler'
75
+
76
+ # Security with Devise
77
+ # authenticate :user do
78
+ # mount MongoProfiler::Web => '/mongo_profiler'
79
+ # end
80
+ #
81
+ # authenticate :user, lambda { |u| u.admin? } do
82
+ # mount MongoProfiler::Web => '/mongo_profiler'
83
+ # end
84
+ end
85
+ ```
86
+
87
+ ## Screenshots
88
+
89
+ ### Dashboard index
90
+
91
+ ![Dashboard Index](https://raw.github.com/phstc/mongo_profiler/master/assets/mongo_profiler_dashboard_index.png)
92
+
93
+ ### Queries Group Index
94
+
95
+ ![Queries Group Index](https://raw.github.com/phstc/mongo_profiler/master/assets/mongo_profiler_group_details.png)
96
+
97
+ ### Query details
98
+
99
+ ![Query Details](https://raw.github.com/phstc/mongo_profiler/master/assets/mongo_profiler_query_details.png)
100
+
101
+ ### Query details (backtrace)
102
+
103
+ ![Query Details Backtrace](https://raw.github.com/phstc/mongo_profiler/master/assets/mongo_profiler_query_details_backtrace.png)
104
+
105
+
106
+ ## Contributing
107
+
108
+ 1. Fork it ( http://github.com/phstc/mongo_profiler/fork )
109
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
110
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
111
+ 4. Push to the branch (`git push origin my-new-feature`)
112
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/config.ru ADDED
@@ -0,0 +1,13 @@
1
+ require 'pry-byebug'
2
+ require 'mongo'
3
+ require 'mongo_profiler'
4
+
5
+ require 'mongo_profiler/web'
6
+
7
+ CONNECTION = Mongo::MongoClient.new
8
+ DB = CONNECTION.db('mongo_profiler-database')
9
+ COLL = DB['example-collection']
10
+
11
+ MongoProfiler.connect('localhost', 27017, 'mongo_profiler-database')
12
+
13
+ run MongoProfiler::Web
@@ -0,0 +1,108 @@
1
+ require 'mongo'
2
+ require 'json'
3
+ require 'active_support/core_ext/hash/indifferent_access'
4
+
5
+ require 'mongo_profiler/version'
6
+ require 'mongo_profiler/profiler'
7
+ require 'mongo_profiler/caller'
8
+ require 'mongo_profiler/payload'
9
+ require 'mongo_profiler/stats'
10
+
11
+ module MongoProfiler
12
+ COLLECTION_CONFIG_NAME = 'mongo_profiler_config'
13
+ COLLECTION_PROFILER_NAME = 'mongo_profiler'
14
+
15
+ class << self
16
+ attr_accessor :extra_attrs,
17
+ :group_id,
18
+ :application_name,
19
+ :stats_client,
20
+ :stats_prefix,
21
+ :graphite_url
22
+
23
+ attr_reader :database
24
+
25
+ def log(document)
26
+ collection.insert(document.merge(application_name: MongoProfiler.application_name,
27
+ group_id: MongoProfiler.group_id))
28
+ end
29
+
30
+ def should_skip?(payload, _caller)
31
+ Payload.new(payload).system_any? || _caller.mongo_profiler_caller?
32
+ end
33
+
34
+ def disable!
35
+ # maybe we can refactor to check if the collections exist before trying to create them.
36
+ # we must make sure the collections are in place before enabled/disable otherwise mongo will create a normal collection,
37
+ # not a capped one, breaking the disable & enable functionality
38
+ create_collections
39
+
40
+ collection_config.insert(enabled: false)
41
+ end
42
+
43
+ def enable!
44
+ # check `disable!` comment
45
+ create_collections
46
+
47
+ collection_config.insert(enabled: true)
48
+ end
49
+
50
+ def enabled?
51
+ !!collection_config.find.first.to_h['enabled']
52
+ end
53
+
54
+ def disabled?
55
+ !enabled?
56
+ end
57
+
58
+ def extra_attrs
59
+ @extra_attrs ||= {}
60
+ end
61
+
62
+ def group_id
63
+ # The group_id is used to determine the life cycle where the queries occurred.
64
+ # For web applications a life cycle can be a request.
65
+ # So people can filter all Mongo Queries per request based on request#url and/or request#uuid.
66
+ @group_id ||= { process_pid: Process.pid,
67
+ thread_object_id: Thread.current.object_id }.to_a.join('-')
68
+ end
69
+
70
+ def stats_client=(stats_client)
71
+ @stats_client = MongoProfiler::Stats.new(stats_client)
72
+ end
73
+
74
+ def create_collections
75
+ # http://docs.mongodb.org/manual/core/capped-collections/
76
+ # 1_048_576 - 1MB - allows only one document (max: 1)
77
+ @database.create_collection(COLLECTION_CONFIG_NAME, capped: true, size: 1_048_576, max: 1)
78
+
79
+ # 4_001_792 - 3.82MB - same size as db.system.profile.stats()
80
+ @database.create_collection(COLLECTION_PROFILER_NAME, capped: true, size: 4_001_792, max: 9223372036854775807)
81
+ end
82
+
83
+ def collection
84
+ @collection ||= @database[COLLECTION_PROFILER_NAME]
85
+ end
86
+
87
+ def collection_config
88
+ @collection_config ||= @database[COLLECTION_CONFIG_NAME]
89
+ end
90
+
91
+ def connected?
92
+ !!(@connection && @database)
93
+ end
94
+
95
+ def connect(host = 'localhost', port = 27017, db = nil, user = nil, pass = nil, options = {})
96
+ @connection, @database = nil
97
+
98
+ @connection = Mongo::MongoClient.new(host, port, options)
99
+ if db
100
+ @database = @connection.db(db)
101
+ else
102
+ # default database
103
+ @database = @connection.db
104
+ end
105
+ @database.authenticate(user, pass) if user && pass
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,27 @@
1
+ module MongoProfiler
2
+ class Caller
3
+ attr_reader :file, :line, :method, :_caller
4
+
5
+ def initialize(_caller)
6
+ @_caller = _caller.dup
7
+
8
+ caller_head = project_callers[0].split ':'
9
+
10
+ # i.e. "/Users/pablo/workspace/project/spec/mongo_profiler_spec.rb:7:in `new'",
11
+ @file = caller_head[0]
12
+ @line = caller_head[1].to_i
13
+ @method = project_callers[0][/`.*'/][1..-2]
14
+ end
15
+
16
+ def mongo_profiler_caller?
17
+ _caller.any? { |line| line.include?('mongo_profiler') && !line.include?('_spec') }
18
+ end
19
+
20
+ private
21
+
22
+ def project_callers
23
+ # skip gem/bundle entries
24
+ @project_callers ||= _caller.select { |line| !line.include?('bundle/ruby') && !line.include?('gem/ruby') && !line.include?('rubies/ruby') }
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,38 @@
1
+ Mongo::Cursor.class_eval do
2
+ alias_method :original_send_initial_query, :send_initial_query
3
+
4
+ def send_initial_query
5
+ beginning_time = Time.now
6
+ original_send_initial_query
7
+ total_time = Time.now - beginning_time
8
+ begin
9
+ _caller = MongoProfiler::Caller.new(caller)
10
+
11
+ return if MongoProfiler.should_skip?(instrument_payload, _caller) || MongoProfiler.disabled?
12
+
13
+ result = {}
14
+
15
+ result[:total_time] = total_time
16
+
17
+ # the payload sent to mongo
18
+ result[:instrument_payload] = JSON.dump(instrument_payload)
19
+
20
+ result[:file] = _caller.file
21
+ result[:line] = _caller.line
22
+ result[:method] = _caller.method
23
+
24
+ result[:extra_attrs] = MongoProfiler.extra_attrs
25
+
26
+ # TODO rename `_caller` object instance to something more meaningful in this context
27
+ result[:backtrace] = _caller._caller
28
+
29
+ MongoProfiler.log(result)
30
+
31
+ if stats_client = MongoProfiler.stats_client
32
+ stats_client.populate(_caller, total_time)
33
+ end
34
+ rescue => e
35
+ p "MongoProfiler: #{e.message}"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,33 @@
1
+ module MongoProfiler
2
+ class Payload
3
+ attr_reader :payload
4
+
5
+ def initialize(payload)
6
+ @payload = (payload || {}).dup.with_indifferent_access
7
+ end
8
+
9
+ def system_database?
10
+ !payload['database'].to_s.match(/^admin|system/).nil?
11
+ end
12
+
13
+ def system_collection?
14
+ !payload['collection'].to_s.match(/^mongo_|system/).nil?
15
+ end
16
+
17
+ def system_count?
18
+ !payload['selector'].to_h['count'].to_s.match(/^mongo_|system/).nil?
19
+ end
20
+
21
+ def system_distinct?
22
+ !payload['selector'].to_h['distinct'].to_s.match(/^mongo_|system/).nil?
23
+ end
24
+
25
+ def system_command?
26
+ payload['collection'] == '$cmd' && !(payload['selector'].to_h.has_key?('count') || payload['selector'].to_h.has_key?('distinct'))
27
+ end
28
+
29
+ def system_any?
30
+ system_database? || system_collection? || system_count? || system_distinct? || system_command?
31
+ end
32
+ end
33
+ end