stinkingtoe 1.11.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.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/.gitignore +5 -0
- data/.ruby-version +1 -0
- data/Gemfile +10 -0
- data/LICENSE +20 -0
- data/README.md +120 -0
- data/Rakefile +60 -0
- data/lib/stinkingtoe/comment.rb +185 -0
- data/lib/stinkingtoe/railtie.rb +74 -0
- data/lib/stinkingtoe/sidekiq_instrumentation.rb +24 -0
- data/lib/stinkingtoe.rb +121 -0
- data/stinkingtoe.gemspec +25 -0
- data/test/query_comments_test.rb +372 -0
- metadata +182 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d88e8ea94e8b670203c820b5aa082e82f0515e5e6b995911977431128eea6909
|
|
4
|
+
data.tar.gz: 80f43ac62f3632ab67527be25ad2c3e9c896f59059c61d798f9927ff5de7d7f7
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7c876857d5c40dfc152150993ea80e58f58ec531b65a00d944914a81e9d865dc9d2e7a725588b853269bdc7cb796fa3a3b77ae69e98d76bc7d21c204f8116888
|
|
7
|
+
data.tar.gz: 07ad331dbbb5cc229f9e68ce4c1dc27f70dec49fb0474d02a5477be4c755d0f9a539c67acb82ffd6f85d1cf3bef78352a8f39bd74f7b695589e27624872d2fa7
|
data/.DS_Store
ADDED
|
Binary file
|
data/.gitignore
ADDED
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.6.8
|
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Copyright (c) 2026
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
# a copy of this software and associated documentation files (the
|
|
5
|
+
# "Software"), to deal in the Software without restriction, including
|
|
6
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
# the following conditions:
|
|
10
|
+
#
|
|
11
|
+
# The above copyright notice and this permission notice shall be
|
|
12
|
+
# included in all copies or substantial portions of the Software.
|
|
13
|
+
#
|
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# stinkingtoe
|
|
2
|
+
|
|
3
|
+
Add comments to your ActiveRecord queries. By default, this appends the application, controller, and action names as a comment to each query.
|
|
4
|
+
|
|
5
|
+
This is useful for tracking queries in log files and identifying the source of slow queries.
|
|
6
|
+
|
|
7
|
+
For example, once enabled, your logs will look like:
|
|
8
|
+
|
|
9
|
+
Account Load (0.3ms) SELECT `accounts`.* FROM `accounts`
|
|
10
|
+
WHERE `accounts`.`queenbee_id` = 1234567890
|
|
11
|
+
LIMIT 1
|
|
12
|
+
/*application:APP,controller:project_imports,action:show*/
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
# Gemfile
|
|
17
|
+
gem 'stinkingtoe'
|
|
18
|
+
|
|
19
|
+
### Customization
|
|
20
|
+
|
|
21
|
+
Optionally, you can set the application name shown in the log like so in an initializer (e.g. `config/initializers/stinkingtoe.rb`):
|
|
22
|
+
|
|
23
|
+
StinkingToe.application_name = "APP"
|
|
24
|
+
|
|
25
|
+
The name will default to your Rails application name.
|
|
26
|
+
|
|
27
|
+
#### Components
|
|
28
|
+
|
|
29
|
+
You can also configure the components of the comment that will be appended,
|
|
30
|
+
by setting `StinkingToe::Comment.components`. By default, this is set to:
|
|
31
|
+
|
|
32
|
+
StinkingToe::Comment.components = [:application, :controller, :action]
|
|
33
|
+
|
|
34
|
+
Which results in a comment of
|
|
35
|
+
`application:#{application_name},controller:#{controller.name},action:#{action_name}`.
|
|
36
|
+
|
|
37
|
+
You can re-order or remove these components. You can also add additional
|
|
38
|
+
comment components of your desire by defining new module methods for
|
|
39
|
+
`StinkingToe::Comment` which return a string. For example:
|
|
40
|
+
|
|
41
|
+
module StinkingToe
|
|
42
|
+
module Comment
|
|
43
|
+
def self.mycommentcomponent
|
|
44
|
+
"TEST"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
StinkingToe::Comment.components = [:application, :mycommentcomponent]
|
|
50
|
+
|
|
51
|
+
Which will result in a comment like
|
|
52
|
+
`application:#{application_name},mycommentcomponent:TEST`
|
|
53
|
+
The calling controller is available to these methods via `@controller`.
|
|
54
|
+
|
|
55
|
+
StinkingToe ships with `:application`, `:controller`, and `:action` enabled by
|
|
56
|
+
default. In addition, implementation is provided for:
|
|
57
|
+
* `:line` (for file and line number calling query). :line supports
|
|
58
|
+
a configuration by setting a regexp in `StinkingToe::Comment.lines_to_ignore`
|
|
59
|
+
to exclude parts of the stacktrace from inclusion in the line comment.
|
|
60
|
+
* `:controller_with_namespace` to include the full classname (including namespace)
|
|
61
|
+
of the controller.
|
|
62
|
+
* `:job` to include the classname of the ActiveJob being performed.
|
|
63
|
+
* `:hostname` to include ```Socket.gethostname```.
|
|
64
|
+
* `:pid` to include current process id.
|
|
65
|
+
* `:db_host` to include the configured database hostname.
|
|
66
|
+
* `:socket` to include the configured database socket.
|
|
67
|
+
* `:database` to include the configured database name.
|
|
68
|
+
|
|
69
|
+
Pull requests for other included comment components are welcome.
|
|
70
|
+
|
|
71
|
+
#### Prepend comments
|
|
72
|
+
|
|
73
|
+
By default, `stinkingtoe` appends comments at the end of queries. Some databases, like MySQL, truncate query text—for example, in slow query logs and certain InnoDB internal table queries where the query exceeds 1024 bytes.
|
|
74
|
+
|
|
75
|
+
In order to not lose the `stinkingtoe` comments from your logs, you can prepend the comments using this option:
|
|
76
|
+
|
|
77
|
+
StinkingToe::Comment.prepend_comment = true
|
|
78
|
+
|
|
79
|
+
#### Inline query annotations
|
|
80
|
+
|
|
81
|
+
In addition to the request or job-level component-based annotations,
|
|
82
|
+
`stinkingtoe` may be used to add inline annotations to specific queries using a
|
|
83
|
+
block-based API.
|
|
84
|
+
|
|
85
|
+
For example, the following code:
|
|
86
|
+
|
|
87
|
+
StinkingToe.with_annotation("foo") do
|
|
88
|
+
Account.where(queenbee_id: 1234567890).first
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
will issue this query:
|
|
92
|
+
|
|
93
|
+
Account Load (0.3ms) SELECT `accounts`.* FROM `accounts`
|
|
94
|
+
WHERE `accounts`.`queenbee_id` = 1234567890
|
|
95
|
+
LIMIT 1
|
|
96
|
+
/*application:APP,controller:project_imports,action:show*/ /*foo*/
|
|
97
|
+
|
|
98
|
+
Nesting `with_annotation` blocks will concatenate the comment strings.
|
|
99
|
+
|
|
100
|
+
### Caveats
|
|
101
|
+
|
|
102
|
+
#### Prepared statements
|
|
103
|
+
|
|
104
|
+
Be careful when using `stinkingtoe` with prepared statements. If you use a component
|
|
105
|
+
like `request_id` then every query will be unique and so ActiveRecord will create
|
|
106
|
+
a new prepared statement for each potentially exhausting system resources.
|
|
107
|
+
[Disable prepared statements](https://guides.rubyonrails.org/configuring.html#configuring-a-postgresql-database)
|
|
108
|
+
if you wish to use components with high cardinality values.
|
|
109
|
+
|
|
110
|
+
## Contributing
|
|
111
|
+
|
|
112
|
+
Start by bundling and creating the test database:
|
|
113
|
+
|
|
114
|
+
bundle
|
|
115
|
+
rake db:mysql:create
|
|
116
|
+
rake db:postgresql:create
|
|
117
|
+
|
|
118
|
+
Then, running `rake` will run the tests on all the database adapters (`mysql`, `mysql2`, `postgresql` and `sqlite`):
|
|
119
|
+
|
|
120
|
+
rake
|
data/Rakefile
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env rake
|
|
2
|
+
require "bundler/gem_tasks"
|
|
3
|
+
|
|
4
|
+
task :default => ['test:all']
|
|
5
|
+
|
|
6
|
+
namespace :test do
|
|
7
|
+
desc "test all drivers"
|
|
8
|
+
task :all => [:mysql2, :postgresql, :sqlite]
|
|
9
|
+
|
|
10
|
+
desc "test mysql2 driver"
|
|
11
|
+
task :mysql2 do
|
|
12
|
+
sh "DRIVER=mysql2 bundle exec ruby -Ilib -Itest test/*_test.rb"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
desc "test PostgreSQL driver"
|
|
16
|
+
task :postgresql do
|
|
17
|
+
sh "DRIVER=postgresql DB_USERNAME=postgres bundle exec ruby -Ilib -Itest test/*_test.rb"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
desc "test sqlite3 driver"
|
|
21
|
+
task :sqlite do
|
|
22
|
+
sh "DRIVER=sqlite3 bundle exec ruby -Ilib -Itest test/*_test.rb"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
namespace :db do
|
|
27
|
+
|
|
28
|
+
desc "reset all databases"
|
|
29
|
+
task :reset => [:"mysql:reset", :"postgresql:reset"]
|
|
30
|
+
|
|
31
|
+
namespace :mysql do
|
|
32
|
+
desc "reset MySQL database"
|
|
33
|
+
task :reset => [:drop, :create]
|
|
34
|
+
|
|
35
|
+
desc "create MySQL database"
|
|
36
|
+
task :create do
|
|
37
|
+
sh 'mysql -u root -e "create database stinkingtoe_test;"'
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
desc "drop MySQL database"
|
|
41
|
+
task :drop do
|
|
42
|
+
sh 'mysql -u root -e "drop database if exists stinkingtoe_test;"'
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
namespace :postgresql do
|
|
47
|
+
desc "reset PostgreSQL database"
|
|
48
|
+
task :reset => [:drop, :create]
|
|
49
|
+
|
|
50
|
+
desc "create PostgreSQL database"
|
|
51
|
+
task :create do
|
|
52
|
+
sh 'createdb -U postgres stinkingtoe_test'
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
desc "drop PostgreSQL database"
|
|
56
|
+
task :drop do
|
|
57
|
+
sh 'psql -d postgres -U postgres -c "DROP DATABASE IF EXISTS stinkingtoe_test"'
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'socket'
|
|
4
|
+
|
|
5
|
+
module StinkingToe
|
|
6
|
+
module Comment
|
|
7
|
+
mattr_accessor :components, :lines_to_ignore, :prepend_comment
|
|
8
|
+
StinkingToe::Comment.components ||= [:application, :controller, :action]
|
|
9
|
+
|
|
10
|
+
def self.update!(controller = nil)
|
|
11
|
+
self.stinkingtoe_controller = controller
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.update_job!(job)
|
|
15
|
+
self.stinkingtoe_job = job
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.update_adapter!(adapter)
|
|
19
|
+
self.stinkingtoe_adapter = adapter
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.construct_comment
|
|
23
|
+
ret = String.new
|
|
24
|
+
self.components.each do |c|
|
|
25
|
+
component_value = self.send(c)
|
|
26
|
+
if component_value.present?
|
|
27
|
+
ret << "#{c}:#{component_value},"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
ret.chop!
|
|
31
|
+
ret = self.escape_sql_comment(ret)
|
|
32
|
+
ret
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.construct_inline_comment
|
|
36
|
+
return nil if inline_annotations.none?
|
|
37
|
+
escape_sql_comment(inline_annotations.join)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.escape_sql_comment(str)
|
|
41
|
+
while str.include?('/*') || str.include?('*/')
|
|
42
|
+
str = str.gsub('/*', '').gsub('*/', '')
|
|
43
|
+
end
|
|
44
|
+
str
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.clear!
|
|
48
|
+
self.stinkingtoe_controller = nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.clear_job!
|
|
52
|
+
self.stinkingtoe_job = nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
def self.stinkingtoe_controller=(controller)
|
|
57
|
+
Thread.current[:stinkingtoe_controller] = controller
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.stinkingtoe_controller
|
|
61
|
+
Thread.current[:stinkingtoe_controller]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.stinkingtoe_job=(job)
|
|
65
|
+
Thread.current[:stinkingtoe_job] = job
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.stinkingtoe_job
|
|
69
|
+
Thread.current[:stinkingtoe_job]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.stinkingtoe_adapter=(adapter)
|
|
73
|
+
Thread.current[:stinkingtoe_adapter] = adapter
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.stinkingtoe_adapter
|
|
77
|
+
Thread.current[:stinkingtoe_adapter]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.application
|
|
81
|
+
if defined?(Rails.application)
|
|
82
|
+
StinkingToe.application_name ||= Rails.application.class.name.split("::").first
|
|
83
|
+
else
|
|
84
|
+
StinkingToe.application_name ||= "rails"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
StinkingToe.application_name
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def self.job
|
|
91
|
+
stinkingtoe_job.class.name if stinkingtoe_job
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.controller
|
|
95
|
+
stinkingtoe_controller.controller_name if stinkingtoe_controller.respond_to? :controller_name
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def self.controller_with_namespace
|
|
99
|
+
stinkingtoe_controller.class.name if stinkingtoe_controller
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def self.action
|
|
103
|
+
stinkingtoe_controller.action_name if stinkingtoe_controller.respond_to? :action_name
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def self.sidekiq_job
|
|
107
|
+
stinkingtoe_job["class"] if stinkingtoe_job && stinkingtoe_job.respond_to?(:[])
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
DEFAULT_LINES_TO_IGNORE_REGEX = %r{\.rvm|/ruby/gems/|vendor/|stinkingtoe|rbenv|monitor\.rb.*mon_synchronize}
|
|
111
|
+
|
|
112
|
+
def self.line
|
|
113
|
+
StinkingToe::Comment.lines_to_ignore ||= DEFAULT_LINES_TO_IGNORE_REGEX
|
|
114
|
+
|
|
115
|
+
last_line = caller_locations.detect do |loc|
|
|
116
|
+
!loc.path.match?(StinkingToe::Comment.lines_to_ignore)
|
|
117
|
+
end
|
|
118
|
+
if last_line
|
|
119
|
+
last_line = last_line.to_s
|
|
120
|
+
|
|
121
|
+
root = if defined?(Rails) && Rails.respond_to?(:root)
|
|
122
|
+
Rails.root.to_s
|
|
123
|
+
elsif defined?(RAILS_ROOT)
|
|
124
|
+
RAILS_ROOT
|
|
125
|
+
else
|
|
126
|
+
""
|
|
127
|
+
end
|
|
128
|
+
if last_line.start_with? root
|
|
129
|
+
last_line = last_line[root.length..-1]
|
|
130
|
+
end
|
|
131
|
+
last_line
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def self.hostname
|
|
136
|
+
@cached_hostname ||= Socket.gethostname
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def self.pid
|
|
140
|
+
Process.pid
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def self.request_id
|
|
144
|
+
if stinkingtoe_controller.respond_to?(:request) && stinkingtoe_controller.request.respond_to?(:uuid)
|
|
145
|
+
stinkingtoe_controller.request.uuid
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def self.socket
|
|
150
|
+
if self.connection_config.present?
|
|
151
|
+
self.connection_config[:socket]
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def self.db_host
|
|
156
|
+
if self.connection_config.present?
|
|
157
|
+
self.connection_config[:host]
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def self.database
|
|
162
|
+
if self.connection_config.present?
|
|
163
|
+
self.connection_config[:database]
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
if Gem::Version.new(ActiveRecord::VERSION::STRING) < Gem::Version.new('6.1')
|
|
168
|
+
def self.connection_config
|
|
169
|
+
return if stinkingtoe_adapter.pool.nil?
|
|
170
|
+
stinkingtoe_adapter.pool.spec.config
|
|
171
|
+
end
|
|
172
|
+
else
|
|
173
|
+
def self.connection_config
|
|
174
|
+
# `pool` might be a NullPool which has no db_config
|
|
175
|
+
return unless stinkingtoe_adapter.pool.respond_to?(:db_config)
|
|
176
|
+
stinkingtoe_adapter.pool.db_config.configuration_hash
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def self.inline_annotations
|
|
181
|
+
Thread.current[:stinkingtoe_inline_annotations] ||= []
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
require 'stinkingtoe'
|
|
2
|
+
require 'rails/railtie'
|
|
3
|
+
|
|
4
|
+
module StinkingToe
|
|
5
|
+
class Railtie < Rails::Railtie
|
|
6
|
+
initializer 'marginalia.insert' do
|
|
7
|
+
ActiveSupport.on_load :active_record do
|
|
8
|
+
StinkingToe::Railtie.insert_into_active_record
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
ActiveSupport.on_load :action_controller do
|
|
12
|
+
StinkingToe::Railtie.insert_into_action_controller
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
ActiveSupport.on_load :active_job do
|
|
16
|
+
StinkingToe::Railtie.insert_into_active_job
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.insert
|
|
21
|
+
insert_into_active_record
|
|
22
|
+
insert_into_action_controller
|
|
23
|
+
insert_into_active_job
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.insert_into_active_job
|
|
27
|
+
if defined? ActiveJob::Base
|
|
28
|
+
ActiveJob::Base.class_eval do
|
|
29
|
+
around_perform do |job, block|
|
|
30
|
+
begin
|
|
31
|
+
StinkingToe::Comment.update_job! job
|
|
32
|
+
block.call
|
|
33
|
+
ensure
|
|
34
|
+
StinkingToe::Comment.clear_job!
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.insert_into_action_controller
|
|
42
|
+
ActionController::Base.send(:include, ActionControllerInstrumentation)
|
|
43
|
+
if defined? ActionController::API
|
|
44
|
+
ActionController::API.send(:include, ActionControllerInstrumentation)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.insert_into_active_record
|
|
49
|
+
if defined? ActiveRecord::ConnectionAdapters::Mysql2Adapter
|
|
50
|
+
ActiveRecord::ConnectionAdapters::Mysql2Adapter.module_eval do
|
|
51
|
+
include StinkingToe::ActiveRecordInstrumentation
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if defined? ActiveRecord::ConnectionAdapters::MysqlAdapter
|
|
56
|
+
ActiveRecord::ConnectionAdapters::MysqlAdapter.module_eval do
|
|
57
|
+
include StinkingToe::ActiveRecordInstrumentation
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if defined? ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
|
|
62
|
+
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.module_eval do
|
|
63
|
+
include StinkingToe::ActiveRecordInstrumentation
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
if defined? ActiveRecord::ConnectionAdapters::SQLite3Adapter
|
|
68
|
+
ActiveRecord::ConnectionAdapters::SQLite3Adapter.module_eval do
|
|
69
|
+
include StinkingToe::ActiveRecordInstrumentation
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module StinkingToe
|
|
2
|
+
|
|
3
|
+
# Alternative to ActiveJob Instrumentation for Sidekiq.
|
|
4
|
+
module SidekiqInstrumentation
|
|
5
|
+
|
|
6
|
+
class Middleware
|
|
7
|
+
def call(worker, msg, queue)
|
|
8
|
+
StinkingToe::Comment.update_job! msg
|
|
9
|
+
yield
|
|
10
|
+
ensure
|
|
11
|
+
StinkingToe::Comment.clear_job!
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.enable!
|
|
16
|
+
Sidekiq.configure_server do |config|
|
|
17
|
+
config.server_middleware do |chain|
|
|
18
|
+
chain.add StinkingToe::SidekiqInstrumentation::Middleware
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
end
|
data/lib/stinkingtoe.rb
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
require 'stinkingtoe/railtie'
|
|
2
|
+
require 'stinkingtoe/comment'
|
|
3
|
+
require 'stinkingtoe/sidekiq_instrumentation'
|
|
4
|
+
|
|
5
|
+
module StinkingToe
|
|
6
|
+
mattr_accessor :application_name
|
|
7
|
+
|
|
8
|
+
module ActiveRecordInstrumentation
|
|
9
|
+
def self.included(instrumented_class)
|
|
10
|
+
instrumented_class.class_eval do
|
|
11
|
+
if instrumented_class.method_defined?(:execute)
|
|
12
|
+
alias_method :execute_without_stinkingtoe, :execute
|
|
13
|
+
alias_method :execute, :execute_with_stinkingtoe
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
if instrumented_class.private_method_defined?(:execute_and_clear)
|
|
17
|
+
alias_method :execute_and_clear_without_stinkingtoe, :execute_and_clear
|
|
18
|
+
alias_method :execute_and_clear, :execute_and_clear_with_stinkingtoe
|
|
19
|
+
else
|
|
20
|
+
is_mysql2 = defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter) &&
|
|
21
|
+
ActiveRecord::ConnectionAdapters::Mysql2Adapter == instrumented_class
|
|
22
|
+
# Dont instrument exec_query on mysql2 as it calls execute internally
|
|
23
|
+
unless is_mysql2
|
|
24
|
+
if instrumented_class.method_defined?(:exec_query)
|
|
25
|
+
alias_method :exec_query_without_stinkingtoe, :exec_query
|
|
26
|
+
alias_method :exec_query, :exec_query_with_stinkingtoe
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
is_postgres = defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) &&
|
|
31
|
+
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter == instrumented_class
|
|
32
|
+
# Instrument exec_delete and exec_update since they don't call
|
|
33
|
+
# execute internally
|
|
34
|
+
if is_postgres
|
|
35
|
+
if instrumented_class.method_defined?(:exec_delete)
|
|
36
|
+
alias_method :exec_delete_without_stinkingtoe, :exec_delete
|
|
37
|
+
alias_method :exec_delete, :exec_delete_with_stinkingtoe
|
|
38
|
+
end
|
|
39
|
+
if instrumented_class.method_defined?(:exec_update)
|
|
40
|
+
alias_method :exec_update_without_stinkingtoe, :exec_update
|
|
41
|
+
alias_method :exec_update, :exec_update_with_stinkingtoe
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def annotate_sql(sql)
|
|
49
|
+
StinkingToe::Comment.update_adapter!(self)
|
|
50
|
+
comment = StinkingToe::Comment.construct_comment
|
|
51
|
+
if comment.present? && !sql.include?(comment)
|
|
52
|
+
sql = if StinkingToe::Comment.prepend_comment
|
|
53
|
+
"/*#{comment}*/ #{sql}"
|
|
54
|
+
else
|
|
55
|
+
"#{sql} /*#{comment}*/"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
inline_comment = StinkingToe::Comment.construct_inline_comment
|
|
59
|
+
if inline_comment.present? && !sql.include?(inline_comment)
|
|
60
|
+
sql = if StinkingToe::Comment.prepend_comment
|
|
61
|
+
"/*#{inline_comment}*/ #{sql}"
|
|
62
|
+
else
|
|
63
|
+
"#{sql} /*#{inline_comment}*/"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
sql
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def execute_with_stinkingtoe(sql, *args)
|
|
71
|
+
execute_without_stinkingtoe(annotate_sql(sql), *args)
|
|
72
|
+
end
|
|
73
|
+
ruby2_keywords :execute_with_stinkingtoe if respond_to?(:ruby2_keywords, true)
|
|
74
|
+
|
|
75
|
+
def exec_query_with_stinkingtoe(sql, *args, **options)
|
|
76
|
+
options[:prepare] ||= false
|
|
77
|
+
exec_query_without_stinkingtoe(annotate_sql(sql), *args, **options)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def exec_delete_with_stinkingtoe(sql, *args)
|
|
81
|
+
exec_delete_without_stinkingtoe(annotate_sql(sql), *args)
|
|
82
|
+
end
|
|
83
|
+
ruby2_keywords :exec_delete_with_stinkingtoe if respond_to?(:ruby2_keywords, true)
|
|
84
|
+
|
|
85
|
+
def exec_update_with_stinkingtoe(sql, *args)
|
|
86
|
+
exec_update_without_stinkingtoe(annotate_sql(sql), *args)
|
|
87
|
+
end
|
|
88
|
+
ruby2_keywords :exec_update_with_stinkingtoe if respond_to?(:ruby2_keywords, true)
|
|
89
|
+
|
|
90
|
+
def execute_and_clear_with_stinkingtoe(sql, *args, &block)
|
|
91
|
+
execute_and_clear_without_stinkingtoe(annotate_sql(sql), *args, &block)
|
|
92
|
+
end
|
|
93
|
+
ruby2_keywords :execute_and_clear_with_stinkingtoe if respond_to?(:ruby2_keywords, true)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
module ActionControllerInstrumentation
|
|
97
|
+
def self.included(instrumented_class)
|
|
98
|
+
instrumented_class.class_eval do
|
|
99
|
+
if respond_to?(:around_action)
|
|
100
|
+
around_action :record_query_comment
|
|
101
|
+
else
|
|
102
|
+
around_filter :record_query_comment
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def record_query_comment
|
|
108
|
+
StinkingToe::Comment.update!(self)
|
|
109
|
+
yield
|
|
110
|
+
ensure
|
|
111
|
+
StinkingToe::Comment.clear!
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def self.with_annotation(comment, &block)
|
|
116
|
+
StinkingToe::Comment.inline_annotations.push(comment)
|
|
117
|
+
block.call if block.present?
|
|
118
|
+
ensure
|
|
119
|
+
StinkingToe::Comment.inline_annotations.pop
|
|
120
|
+
end
|
|
121
|
+
end
|
data/stinkingtoe.gemspec
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Gem::Specification.new do |gem|
|
|
2
|
+
gem.authors = ["Linda Zhou"]
|
|
3
|
+
gem.email = ["lhmzhou@github.com"]
|
|
4
|
+
gem.homepage = "https://github.com/lhmzhou"
|
|
5
|
+
|
|
6
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
|
7
|
+
gem.files = `git ls-files`.split("\n")
|
|
8
|
+
gem.test_files = `git ls-files -- {test}/*`.split("\n")
|
|
9
|
+
gem.name = "stinkingtoe"
|
|
10
|
+
gem.require_paths = ["lib"]
|
|
11
|
+
gem.version = "1.11.1"
|
|
12
|
+
gem.license = "MIT"
|
|
13
|
+
|
|
14
|
+
gem.add_runtime_dependency "actionpack", ">= 5.2"
|
|
15
|
+
gem.add_runtime_dependency "activerecord", ">= 5.2"
|
|
16
|
+
gem.add_development_dependency "rake"
|
|
17
|
+
gem.add_development_dependency "mysql2"
|
|
18
|
+
gem.add_development_dependency "pg"
|
|
19
|
+
gem.add_development_dependency "sqlite3"
|
|
20
|
+
gem.add_development_dependency "minitest"
|
|
21
|
+
gem.add_development_dependency "mocha"
|
|
22
|
+
gem.add_development_dependency "sidekiq"
|
|
23
|
+
|
|
24
|
+
gem.summary = gem.description = %q{attach comments to your ActiveRecord queries}
|
|
25
|
+
end
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
require 'rails/version'
|
|
3
|
+
|
|
4
|
+
def using_rails_api?
|
|
5
|
+
ENV["TEST_RAILS_API"] == true
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def pool_db_config?
|
|
9
|
+
Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new('6.1')
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
require "minitest/autorun"
|
|
13
|
+
require "mocha/minitest"
|
|
14
|
+
require 'logger'
|
|
15
|
+
require 'pp'
|
|
16
|
+
require 'active_record'
|
|
17
|
+
require 'action_controller'
|
|
18
|
+
require 'active_job'
|
|
19
|
+
require 'sidekiq'
|
|
20
|
+
require 'sidekiq/testing'
|
|
21
|
+
|
|
22
|
+
require 'action_dispatch/middleware/request_id'
|
|
23
|
+
|
|
24
|
+
if using_rails_api?
|
|
25
|
+
require 'rails-api/action_controller/api'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Shim for compatibility with older versions of Minitest
|
|
29
|
+
Minitest::Test = Minitest::Unit::TestCase unless defined?(Minitest::Test)
|
|
30
|
+
|
|
31
|
+
# From version 4.1, ActiveRecord expects `Rails.env` to be
|
|
32
|
+
# defined if `Rails` is defined
|
|
33
|
+
if defined?(Rails) && !defined?(Rails.env)
|
|
34
|
+
module Rails
|
|
35
|
+
def self.env
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
require 'stinkingtoe'
|
|
41
|
+
RAILS_ROOT = File.expand_path(File.dirname(__FILE__))
|
|
42
|
+
|
|
43
|
+
ActiveRecord::Base.establish_connection({
|
|
44
|
+
:adapter => ENV["DRIVER"] || "mysql",
|
|
45
|
+
:host => ENV["DB_HOST"] || "localhost",
|
|
46
|
+
:username => ENV["DB_USERNAME"] || "root",
|
|
47
|
+
:database => "stinkingtoe_test"
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
class Post < ActiveRecord::Base
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class PostsController < ActionController::Base
|
|
54
|
+
def driver_only
|
|
55
|
+
ActiveRecord::Base.connection.execute "select id from posts"
|
|
56
|
+
if Gem::Version.new(Rails::VERSION::STRING) >= Gem::Version.new('5')
|
|
57
|
+
render body: nil
|
|
58
|
+
else
|
|
59
|
+
render nothing: true
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
module API
|
|
65
|
+
module V1
|
|
66
|
+
class PostsController < ::PostsController
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
class PostsJob < ActiveJob::Base
|
|
72
|
+
def perform
|
|
73
|
+
Post.first
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
class PostsSidekiqJob
|
|
78
|
+
include Sidekiq::Worker
|
|
79
|
+
def perform
|
|
80
|
+
Post.first
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
if using_rails_api?
|
|
85
|
+
class PostsApiController < ActionController::API
|
|
86
|
+
def driver_only
|
|
87
|
+
ActiveRecord::Base.connection.execute "select id from posts"
|
|
88
|
+
head :no_content
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
unless Post.table_exists?
|
|
94
|
+
ActiveRecord::Schema.define do
|
|
95
|
+
create_table "posts", :force => true do |t|
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
StinkingToe::Railtie.insert
|
|
101
|
+
|
|
102
|
+
class StinkingToeTest < Minitest::Test
|
|
103
|
+
def setup
|
|
104
|
+
# Touch the model to avoid spurious schema queries
|
|
105
|
+
Post.first
|
|
106
|
+
|
|
107
|
+
@queries = []
|
|
108
|
+
ActiveSupport::Notifications.subscribe "sql.active_record" do |*args|
|
|
109
|
+
@queries << args.last[:sql]
|
|
110
|
+
end
|
|
111
|
+
@env = Rack::MockRequest.env_for('/')
|
|
112
|
+
ActiveJob::Base.queue_adapter = :inline
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def test_double_annotate
|
|
116
|
+
ActiveRecord::Base.connection.expects(:annotate_sql).returns("select id from posts").once
|
|
117
|
+
ActiveRecord::Base.connection.send(:select, "select id from posts")
|
|
118
|
+
ensure
|
|
119
|
+
ActiveRecord::Base.connection.unstub(:annotate_sql)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def test_exists
|
|
123
|
+
Post.exists?
|
|
124
|
+
assert_match %r{/\*application:rails\*/$}, @queries.last
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def test_query_commenting_on_mysql_driver_with_no_action
|
|
128
|
+
ActiveRecord::Base.connection.execute "select id from posts"
|
|
129
|
+
assert_match %r{select id from posts /\*application:rails\*/$}, @queries.first
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
if ENV["DRIVER"] =~ /^mysql/
|
|
133
|
+
def test_query_commenting_on_mysql_driver_with_binary_chars
|
|
134
|
+
ActiveRecord::Base.connection.execute "select id from posts /* \x81\x80\u0010\ */"
|
|
135
|
+
assert_equal "select id from posts /* \x81\x80\u0010 */ /*application:rails*/", @queries.first
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
if ENV["DRIVER"] =~ /^postgres/
|
|
140
|
+
def test_query_commenting_on_postgres_update
|
|
141
|
+
ActiveRecord::Base.connection.expects(:annotate_sql).returns("update posts set id = 1").once
|
|
142
|
+
ActiveRecord::Base.connection.send(:exec_update, "update posts set id = 1")
|
|
143
|
+
ensure
|
|
144
|
+
ActiveRecord::Base.connection.unstub(:annotate_sql)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def test_query_commenting_on_postgres_delete
|
|
148
|
+
ActiveRecord::Base.connection.expects(:annotate_sql).returns("delete from posts where id = 1").once
|
|
149
|
+
ActiveRecord::Base.connection.send(:exec_delete, "delete from posts where id = 1")
|
|
150
|
+
ensure
|
|
151
|
+
ActiveRecord::Base.connection.unstub(:annotate_sql)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def test_query_commenting_on_mysql_driver_with_action
|
|
156
|
+
PostsController.action(:driver_only).call(@env)
|
|
157
|
+
assert_match %r{select id from posts /\*application:rails,controller:posts,action:driver_only\*/$}, @queries.first
|
|
158
|
+
|
|
159
|
+
if using_rails_api?
|
|
160
|
+
PostsApiController.action(:driver_only).call(@env)
|
|
161
|
+
assert_match %r{select id from posts /\*application:rails,controller:posts_api,action:driver_only\*/$}, @queries.second
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def test_configuring_application
|
|
166
|
+
StinkingToe.application_name = "customapp"
|
|
167
|
+
PostsController.action(:driver_only).call(@env)
|
|
168
|
+
assert_match %r{/\*application:customapp,controller:posts,action:driver_only\*/$}, @queries.first
|
|
169
|
+
|
|
170
|
+
if using_rails_api?
|
|
171
|
+
PostsApiController.action(:driver_only).call(@env)
|
|
172
|
+
assert_match %r{/\*application:customapp,controller:posts_api,action:driver_only\*/$}, @queries.second
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def test_configuring_query_components
|
|
177
|
+
StinkingToe::Comment.components = [:controller]
|
|
178
|
+
PostsController.action(:driver_only).call(@env)
|
|
179
|
+
assert_match %r{/\*controller:posts\*/$}, @queries.first
|
|
180
|
+
|
|
181
|
+
if using_rails_api?
|
|
182
|
+
PostsApiController.action(:driver_only).call(@env)
|
|
183
|
+
assert_match %r{/\*controller:posts_api\*/$}, @queries.second
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def test_last_line_component
|
|
188
|
+
StinkingToe::Comment.components = [:line]
|
|
189
|
+
PostsController.action(:driver_only).call(@env)
|
|
190
|
+
|
|
191
|
+
# Because "lines_to_ignore" by default includes "stinkingtoe" and "gem", the
|
|
192
|
+
# extracted line line will be from the line in this file that actually
|
|
193
|
+
# triggers the query.
|
|
194
|
+
assert_match %r{/\*line:test/query_comments_test.rb:[0-9]+:in `driver_only'\*/$}, @queries.first
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def test_last_line_component_with_lines_to_ignore
|
|
198
|
+
StinkingToe::Comment.lines_to_ignore = /foo bar/
|
|
199
|
+
StinkingToe::Comment.components = [:line]
|
|
200
|
+
PostsController.action(:driver_only).call(@env)
|
|
201
|
+
# Because "lines_to_ignore" does not include "stinkingtoe", the extracted
|
|
202
|
+
# line will be from stinkingtoe/comment.rb.
|
|
203
|
+
assert_match %r{/\*line:.*lib/stinkingtoe/comment.rb:[0-9]+}, @queries.first
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def test_default_lines_to_ignore_regex
|
|
207
|
+
line = "/gems/a_gem/lib/a_gem.rb:1:in `some_method'"
|
|
208
|
+
call_stack = [line] + caller
|
|
209
|
+
|
|
210
|
+
assert_match(
|
|
211
|
+
call_stack.detect { |line| line !~ StinkingToe::Comment::DEFAULT_LINES_TO_IGNORE_REGEX },
|
|
212
|
+
line
|
|
213
|
+
)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def test_hostname_and_pid
|
|
217
|
+
StinkingToe::Comment.components = [:hostname, :pid]
|
|
218
|
+
PostsController.action(:driver_only).call(@env)
|
|
219
|
+
assert_match %r{/\*hostname:#{Socket.gethostname},pid:#{Process.pid}\*/$}, @queries.first
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def test_controller_with_namespace
|
|
223
|
+
StinkingToe::Comment.components = [:controller_with_namespace]
|
|
224
|
+
API::V1::PostsController.action(:driver_only).call(@env)
|
|
225
|
+
assert_match %r{/\*controller_with_namespace:API::V1::PostsController}, @queries.first
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def test_db_host
|
|
229
|
+
StinkingToe::Comment.components = [:db_host]
|
|
230
|
+
API::V1::PostsController.action(:driver_only).call(@env)
|
|
231
|
+
assert_match %r{/\*db_host:#{ENV["DB_HOST"] || "localhost"}}, @queries.first
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def test_database
|
|
235
|
+
StinkingToe::Comment.components = [:database]
|
|
236
|
+
API::V1::PostsController.action(:driver_only).call(@env)
|
|
237
|
+
assert_match %r{/\*database:stinkingtoe_test}, @queries.first
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
if pool_db_config?
|
|
241
|
+
def test_socket
|
|
242
|
+
# setting socket in configuration would break some connections - mock it instead
|
|
243
|
+
pool = ActiveRecord::Base.connection_pool
|
|
244
|
+
pool.db_config.stubs(:configuration_hash).returns({:socket => "stinkingtoe_socket"})
|
|
245
|
+
StinkingToe::Comment.components = [:socket]
|
|
246
|
+
API::V1::PostsController.action(:driver_only).call(@env)
|
|
247
|
+
assert_match %r{/\*socket:stinkingtoe_socket}, @queries.first
|
|
248
|
+
pool.db_config.unstub(:configuration_hash)
|
|
249
|
+
end
|
|
250
|
+
else
|
|
251
|
+
def test_socket
|
|
252
|
+
# setting socket in configuration would break some connections - mock it instead
|
|
253
|
+
pool = ActiveRecord::Base.connection_pool
|
|
254
|
+
pool.spec.stubs(:config).returns({:socket => "stinkingtoe_socket"})
|
|
255
|
+
StinkingToe::Comment.components = [:socket]
|
|
256
|
+
API::V1::PostsController.action(:driver_only).call(@env)
|
|
257
|
+
assert_match %r{/\*socket:stinkingtoe_socket}, @queries.first
|
|
258
|
+
pool.spec.unstub(:config)
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def test_request_id
|
|
263
|
+
@env["action_dispatch.request_id"] = "some-uuid"
|
|
264
|
+
StinkingToe::Comment.components = [:request_id]
|
|
265
|
+
PostsController.action(:driver_only).call(@env)
|
|
266
|
+
assert_match %r{/\*request_id:some-uuid.*}, @queries.first
|
|
267
|
+
|
|
268
|
+
if using_rails_api?
|
|
269
|
+
PostsApiController.action(:driver_only).call(@env)
|
|
270
|
+
assert_match %r{/\*request_id:some-uuid.*}, @queries.second
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def test_active_job
|
|
275
|
+
StinkingToe::Comment.components = [:job]
|
|
276
|
+
PostsJob.perform_later
|
|
277
|
+
assert_match %{job:PostsJob}, @queries.first
|
|
278
|
+
|
|
279
|
+
Post.first
|
|
280
|
+
refute_match %{job:PostsJob}, @queries.last
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def test_active_job_with_sidekiq
|
|
284
|
+
StinkingToe::Comment.components = [:job, :sidekiq_job]
|
|
285
|
+
PostsJob.perform_later
|
|
286
|
+
assert_match %{job:PostsJob}, @queries.first
|
|
287
|
+
|
|
288
|
+
Post.first
|
|
289
|
+
refute_match %{job:PostsJob}, @queries.last
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def test_sidekiq_job
|
|
293
|
+
StinkingToe::Comment.components = [:sidekiq_job]
|
|
294
|
+
StinkingToe::SidekiqInstrumentation.enable!
|
|
295
|
+
|
|
296
|
+
# Test harness does not run Sidekiq middlewares by default so include testing middleware.
|
|
297
|
+
Sidekiq::Testing.server_middleware do |chain|
|
|
298
|
+
chain.add StinkingToe::SidekiqInstrumentation::Middleware
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
Sidekiq::Testing.fake!
|
|
302
|
+
PostsSidekiqJob.perform_async
|
|
303
|
+
PostsSidekiqJob.drain
|
|
304
|
+
assert_match %{sidekiq_job:PostsSidekiqJob}, @queries.first
|
|
305
|
+
|
|
306
|
+
Post.first
|
|
307
|
+
refute_match %{sidekiq_job:PostsSidekiqJob}, @queries.last
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def test_good_comment
|
|
311
|
+
assert_equal StinkingToe::Comment.escape_sql_comment('app:foo'), 'app:foo'
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def test_bad_comments
|
|
315
|
+
assert_equal StinkingToe::Comment.escape_sql_comment('*/; DROP TABLE USERS;/*'), '; DROP TABLE USERS;'
|
|
316
|
+
assert_equal StinkingToe::Comment.escape_sql_comment('**//; DROP TABLE USERS;/*'), '; DROP TABLE USERS;'
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def test_inline_annotations
|
|
320
|
+
StinkingToe.with_annotation("foo") do
|
|
321
|
+
Post.first
|
|
322
|
+
end
|
|
323
|
+
Post.first
|
|
324
|
+
assert_match %r{/\*foo\*/$}, @queries.first
|
|
325
|
+
refute_match %r{/\*foo\*/$}, @queries.last
|
|
326
|
+
# Assert we're not adding an empty comment, either
|
|
327
|
+
refute_match %r{/\*\s*\*/$}, @queries.last
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def test_nested_inline_annotations
|
|
331
|
+
StinkingToe.with_annotation("foo") do
|
|
332
|
+
StinkingToe.with_annotation("bar") do
|
|
333
|
+
Post.first
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
assert_match %r{/\*foobar\*/$}, @queries.first
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def test_bad_inline_annotations
|
|
340
|
+
StinkingToe.with_annotation("*/; DROP TABLE USERS;/*") do
|
|
341
|
+
Post.first
|
|
342
|
+
end
|
|
343
|
+
StinkingToe.with_annotation("**//; DROP TABLE USERS;//**") do
|
|
344
|
+
Post.first
|
|
345
|
+
end
|
|
346
|
+
assert_match %r{/\*; DROP TABLE USERS;\*/$}, @queries.first
|
|
347
|
+
assert_match %r{/\*; DROP TABLE USERS;\*/$}, @queries.last
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def test_inline_annotations_are_deduped
|
|
351
|
+
StinkingToe.with_annotation("foo") do
|
|
352
|
+
ActiveRecord::Base.connection.execute "select id from posts /*foo*/"
|
|
353
|
+
end
|
|
354
|
+
assert_match %r{select id from posts /\*foo\*/ /\*application:rails\*/$}, @queries.first
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def test_add_comments_to_beginning_of_query
|
|
358
|
+
StinkingToe::Comment.prepend_comment = true
|
|
359
|
+
|
|
360
|
+
ActiveRecord::Base.connection.execute "select id from posts"
|
|
361
|
+
assert_match %r{/\*application:rails\*/ select id from posts$}, @queries.first
|
|
362
|
+
ensure
|
|
363
|
+
StinkingToe::Comment.prepend_comment = nil
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def teardown
|
|
367
|
+
StinkingToe.application_name = nil
|
|
368
|
+
StinkingToe::Comment.lines_to_ignore = nil
|
|
369
|
+
StinkingToe::Comment.components = [:application, :controller, :action]
|
|
370
|
+
ActiveSupport::Notifications.unsubscribe "sql.active_record"
|
|
371
|
+
end
|
|
372
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: stinkingtoe
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.11.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Linda Zhou
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-01-28 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: actionpack
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '5.2'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '5.2'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: activerecord
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '5.2'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '5.2'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rake
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: mysql2
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: pg
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: sqlite3
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: minitest
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - ">="
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '0'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - ">="
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '0'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: mocha
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - ">="
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '0'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - ">="
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '0'
|
|
125
|
+
- !ruby/object:Gem::Dependency
|
|
126
|
+
name: sidekiq
|
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - ">="
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '0'
|
|
132
|
+
type: :development
|
|
133
|
+
prerelease: false
|
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - ">="
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '0'
|
|
139
|
+
description: attach comments to your ActiveRecord queries
|
|
140
|
+
email:
|
|
141
|
+
- lhmzhou@github.com
|
|
142
|
+
executables: []
|
|
143
|
+
extensions: []
|
|
144
|
+
extra_rdoc_files: []
|
|
145
|
+
files:
|
|
146
|
+
- ".DS_Store"
|
|
147
|
+
- ".gitignore"
|
|
148
|
+
- ".ruby-version"
|
|
149
|
+
- Gemfile
|
|
150
|
+
- LICENSE
|
|
151
|
+
- README.md
|
|
152
|
+
- Rakefile
|
|
153
|
+
- lib/stinkingtoe.rb
|
|
154
|
+
- lib/stinkingtoe/comment.rb
|
|
155
|
+
- lib/stinkingtoe/railtie.rb
|
|
156
|
+
- lib/stinkingtoe/sidekiq_instrumentation.rb
|
|
157
|
+
- stinkingtoe.gemspec
|
|
158
|
+
- test/query_comments_test.rb
|
|
159
|
+
homepage: https://github.com/lhmzhou
|
|
160
|
+
licenses:
|
|
161
|
+
- MIT
|
|
162
|
+
metadata: {}
|
|
163
|
+
post_install_message:
|
|
164
|
+
rdoc_options: []
|
|
165
|
+
require_paths:
|
|
166
|
+
- lib
|
|
167
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
168
|
+
requirements:
|
|
169
|
+
- - ">="
|
|
170
|
+
- !ruby/object:Gem::Version
|
|
171
|
+
version: '0'
|
|
172
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
173
|
+
requirements:
|
|
174
|
+
- - ">="
|
|
175
|
+
- !ruby/object:Gem::Version
|
|
176
|
+
version: '0'
|
|
177
|
+
requirements: []
|
|
178
|
+
rubygems_version: 3.0.3.1
|
|
179
|
+
signing_key:
|
|
180
|
+
specification_version: 4
|
|
181
|
+
summary: attach comments to your ActiveRecord queries
|
|
182
|
+
test_files: []
|