mongo_profiler 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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