dial 0.3.1 → 0.4.0
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 +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +4 -2
- data/lib/dial/configuration.rb +3 -1
- data/lib/dial/constants.rb +1 -1
- data/lib/dial/engine/routes.rb +25 -6
- data/lib/dial/middleware/panel.rb +1 -1
- data/lib/dial/middleware.rb +50 -15
- data/lib/dial/prosopite.rb +2 -0
- data/lib/dial/prosopite_logger.rb +15 -0
- data/lib/dial/railtie.rb +10 -20
- data/lib/dial/version.rb +1 -1
- data/lib/dial.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dc0b70ff1d770bd0fc85123280a07bb683d649a059df325b2b9cad938ea346e2
|
4
|
+
data.tar.gz: ef4ab7cdb79f4e773b9d19d35d40624085a1ea15b47798fc9a092c476e4d5f25
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5fd19ba186d0689ad63827557f944c0fcee39c41c0d77053450d1a81ff52bb74442f26e7eb4bb377efacea763f87903d467c858834da56483a3d4ddf01307acc
|
7
|
+
data.tar.gz: 45ccf2409afdc88cca32063aa3583737f00eaffea57f10701a9d6d2c45d066586c85e52eea2387ee9cacb2bc0712feca2159c16e711c52c1c8e2c4f8e34ef162
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,17 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.4.0] - 2025-08-30
|
4
|
+
|
5
|
+
- Add sampling configuration to control percentage of requests profiled
|
6
|
+
- Replace response body buffering with streaming response wrapper
|
7
|
+
- Make prosopite logging thread-safe with thread-local storage
|
8
|
+
- Fix missing namespace for VERNIER_PROFILE_OUT_FILE_EXTENSION in engine routes
|
9
|
+
- Improve error handling and input validation
|
10
|
+
|
11
|
+
## [0.3.2] - 2025-05-14
|
12
|
+
|
13
|
+
- Consolidate railtie initializers
|
14
|
+
|
3
15
|
## [0.3.1] - 2025-05-03
|
4
16
|
|
5
17
|
- Only show truncated summary + one full query in N+1 logs
|
data/README.md
CHANGED
@@ -43,6 +43,7 @@ mount Dial::Engine, at: "/" if Rails.env.development?
|
|
43
43
|
# config/initializers/dial.rb
|
44
44
|
|
45
45
|
Dial.configure do |config|
|
46
|
+
config.sampling_percentage = 50
|
46
47
|
config.vernier_interval = 100
|
47
48
|
config.vernier_allocation_interval = 10_000
|
48
49
|
config.prosopite_ignore_queries += [/pg_sleep/i]
|
@@ -53,10 +54,11 @@ end
|
|
53
54
|
|
54
55
|
Option | Description | Default
|
55
56
|
:- | :- | :-
|
57
|
+
`sampling_percentage` | Percentage of requests to profile. | `100` in development, `1` in production
|
58
|
+
`content_security_policy_nonce` | Sets the content security policy nonce to use when inserting Dial's script. Can be a string, or a Proc which receives `env` and response `headers` as arguments and returns the nonce string. | Rails generated nonce or `nil`
|
56
59
|
`vernier_interval` | Sets the `interval` option for vernier. | `200`
|
57
60
|
`vernier_allocation_interval` | Sets the `allocation_interval` option for vernier. | `2_000`
|
58
61
|
`prosopite_ignore_queries` | Sets the `ignore_queries` option for prosopite. | `[/schema_migrations/i]`
|
59
|
-
`content_security_policy_nonce` | Sets the content security policy nonce to use when inserting Dial's script. Can be a string, or a Proc which receives `env` and response `headers` as arguments and returns the nonce string. | Rails generated nonce or `nil`
|
60
62
|
|
61
63
|
## Comparison with [rack-mini-profiler](https://github.com/MiniProfiler/rack-mini-profiler)
|
62
64
|
|
@@ -70,7 +72,7 @@ Option | Description | Default
|
|
70
72
|
| Memory Profiling | Yes (with memory_profiler) | Yes (*overall usage only) (via vernier hook - graph) |
|
71
73
|
| View Profiling | Yes | Yes (via vernier hook - marker table, chart) |
|
72
74
|
| Snapshot Sampling | Yes | No |
|
73
|
-
| Production Support | Yes | No
|
75
|
+
| Production Support | Yes | No |
|
74
76
|
|
75
77
|
> [!NOTE]
|
76
78
|
> SQL queries displayed in the profile are not annotated with the caller location by default. If you're not using the
|
data/lib/dial/configuration.rb
CHANGED
@@ -12,10 +12,11 @@ module Dial
|
|
12
12
|
class Configuration
|
13
13
|
def initialize
|
14
14
|
@options = {
|
15
|
+
sampling_percentage: ::Rails.env.development? ? 100 : 1,
|
16
|
+
content_security_policy_nonce: -> (env, _headers) { env[NONCE] || "" },
|
15
17
|
vernier_interval: VERNIER_INTERVAL,
|
16
18
|
vernier_allocation_interval: VERNIER_ALLOCATION_INTERVAL,
|
17
19
|
prosopite_ignore_queries: PROSOPITE_IGNORE_QUERIES,
|
18
|
-
content_security_policy_nonce: -> (env, _headers) { env[NONCE] || "" },
|
19
20
|
}
|
20
21
|
|
21
22
|
@options.keys.each do |key|
|
@@ -31,6 +32,7 @@ module Dial
|
|
31
32
|
|
32
33
|
def freeze
|
33
34
|
@options.freeze
|
35
|
+
|
34
36
|
super
|
35
37
|
end
|
36
38
|
end
|
data/lib/dial/constants.rb
CHANGED
@@ -21,8 +21,8 @@ module Dial
|
|
21
21
|
VERNIER_INTERVAL = 200
|
22
22
|
VERNIER_ALLOCATION_INTERVAL = 2_000
|
23
23
|
VERNIER_PROFILE_OUT_RELATIVE_DIRNAME = "tmp/dial/profiles"
|
24
|
+
VERNIER_PROFILE_OUT_FILE_EXTENSION = ".json.gz"
|
24
25
|
VERNIER_VIEWER_URL = "https://vernier.prof"
|
25
26
|
|
26
27
|
PROSOPITE_IGNORE_QUERIES = [/schema_migrations/i].freeze
|
27
|
-
PROSOPITE_LOG_IO = StringIO.new
|
28
28
|
end
|
data/lib/dial/engine/routes.rb
CHANGED
@@ -4,14 +4,33 @@ Dial::Engine.routes.draw do
|
|
4
4
|
scope path: "/dial", as: "dial" do
|
5
5
|
get "profile", to: lambda { |env|
|
6
6
|
uuid = env[::Rack::QUERY_STRING].sub "uuid=", ""
|
7
|
-
path = String ::Rails.root.join Dial::VERNIER_PROFILE_OUT_RELATIVE_DIRNAME, "#{uuid}.json.gz"
|
8
7
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
8
|
+
# Validate UUID format (should end with _vernier)
|
9
|
+
unless uuid.match?(/\A[0-9a-f-]+_vernier\z/)
|
10
|
+
return [
|
11
|
+
400,
|
12
|
+
{ "Content-Type" => "text/plain" },
|
13
|
+
["Bad Request"]
|
14
14
|
]
|
15
|
+
end
|
16
|
+
|
17
|
+
path = String ::Rails.root.join Dial::VERNIER_PROFILE_OUT_RELATIVE_DIRNAME, (uuid + Dial::VERNIER_PROFILE_OUT_FILE_EXTENSION)
|
18
|
+
|
19
|
+
if File.exist? path
|
20
|
+
begin
|
21
|
+
content = File.read path
|
22
|
+
[
|
23
|
+
200,
|
24
|
+
{ "Content-Type" => "application/json", "Access-Control-Allow-Origin" => Dial::VERNIER_VIEWER_URL },
|
25
|
+
[content]
|
26
|
+
]
|
27
|
+
rescue
|
28
|
+
[
|
29
|
+
500,
|
30
|
+
{ "Content-Type" => "text/plain" },
|
31
|
+
["Internal Server Error"]
|
32
|
+
]
|
33
|
+
end
|
15
34
|
else
|
16
35
|
[
|
17
36
|
404,
|
@@ -192,7 +192,7 @@ module Dial
|
|
192
192
|
def formatted_profile_output env, profile_out_filename
|
193
193
|
url_base = ::Rails.application.routes.url_helpers.dial_url host: env[::Rack::HTTP_HOST]
|
194
194
|
prefix = "/" unless url_base.end_with? "/"
|
195
|
-
uuid = profile_out_filename.delete_suffix
|
195
|
+
uuid = profile_out_filename.delete_suffix VERNIER_PROFILE_OUT_FILE_EXTENSION
|
196
196
|
profile_out_url = URI.encode_www_form_component url_base + "#{prefix}dial/profile?uuid=#{uuid}"
|
197
197
|
|
198
198
|
"<a href='https://vernier.prof/from-url/#{profile_out_url}' target='_blank'>View profile</a>"
|
data/lib/dial/middleware.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
require "vernier"
|
4
4
|
require "prosopite"
|
5
5
|
|
6
|
+
require_relative "prosopite"
|
6
7
|
require_relative "middleware/panel"
|
7
8
|
require_relative "middleware/ruby_stat"
|
8
9
|
require_relative "middleware/rails_stat"
|
@@ -21,9 +22,13 @@ module Dial
|
|
21
22
|
return @app.call env
|
22
23
|
end
|
23
24
|
|
25
|
+
unless should_profile?
|
26
|
+
return @app.call env
|
27
|
+
end
|
28
|
+
|
24
29
|
start_time = Process.clock_gettime Process::CLOCK_MONOTONIC
|
25
30
|
|
26
|
-
profile_out_filename = "#{Util.uuid}_vernier
|
31
|
+
profile_out_filename = "#{Util.uuid}_vernier" + VERNIER_PROFILE_OUT_FILE_EXTENSION
|
27
32
|
profile_out_pathname = "#{profile_out_dir_pathname}/#{profile_out_filename}"
|
28
33
|
|
29
34
|
status, headers, rack_body, ruby_vm_stat, gc_stat, gc_stat_heap, vernier_result = nil
|
@@ -48,19 +53,12 @@ module Dial
|
|
48
53
|
finish_time = Process.clock_gettime Process::CLOCK_MONOTONIC
|
49
54
|
env[REQUEST_TIMING] = ((finish_time - start_time) * 1_000).round 2
|
50
55
|
|
51
|
-
|
52
|
-
|
53
|
-
rack_body.close if rack_body.respond_to? :close
|
54
|
-
|
55
|
-
str.sub! "</body>", <<~HTML
|
56
|
-
#{Panel.html env, headers, profile_out_filename, query_logs, ruby_vm_stat, gc_stat, gc_stat_heap, server_timing}
|
57
|
-
</body>
|
58
|
-
HTML
|
59
|
-
end
|
56
|
+
panel_html = Panel.html env, headers, profile_out_filename, query_logs, ruby_vm_stat, gc_stat, gc_stat_heap, server_timing
|
57
|
+
body = PanelInjector.new rack_body, panel_html
|
60
58
|
|
61
|
-
headers
|
59
|
+
headers.delete CONTENT_LENGTH
|
62
60
|
|
63
|
-
[status, headers,
|
61
|
+
[status, headers, body]
|
64
62
|
end
|
65
63
|
|
66
64
|
private
|
@@ -89,13 +87,13 @@ module Dial
|
|
89
87
|
def clear_query_logs!
|
90
88
|
[].tap do |query_logs|
|
91
89
|
entry = section = count = nil
|
92
|
-
|
90
|
+
ProsopiteLogger.log_io.string.lines.each do |line|
|
93
91
|
entry, section, count = process_query_log_line line, entry, section, count
|
94
92
|
query_logs << entry if entry && section.nil?
|
95
93
|
end
|
96
94
|
|
97
|
-
|
98
|
-
|
95
|
+
ProsopiteLogger.log_io.truncate 0
|
96
|
+
ProsopiteLogger.log_io.rewind
|
99
97
|
end
|
100
98
|
end
|
101
99
|
|
@@ -126,5 +124,42 @@ module Dial
|
|
126
124
|
def profile_out_dir_pathname
|
127
125
|
::Rails.root.join VERNIER_PROFILE_OUT_RELATIVE_DIRNAME
|
128
126
|
end
|
127
|
+
|
128
|
+
def should_profile?
|
129
|
+
rand(100) < Dial._configuration.sampling_percentage
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
class PanelInjector
|
134
|
+
def initialize original_body, panel_html
|
135
|
+
@original_body = original_body
|
136
|
+
@panel_html = panel_html
|
137
|
+
@injected = false
|
138
|
+
end
|
139
|
+
|
140
|
+
def each
|
141
|
+
@original_body.each do |chunk|
|
142
|
+
if !@injected && chunk.include?("</body>")
|
143
|
+
@injected = true
|
144
|
+
yield chunk.sub("</body>", "#{@panel_html}\n</body>")
|
145
|
+
else
|
146
|
+
yield chunk
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
yield @panel_html unless @injected
|
151
|
+
ensure
|
152
|
+
close
|
153
|
+
end
|
154
|
+
|
155
|
+
def close
|
156
|
+
@original_body.close if @original_body.respond_to? :close
|
157
|
+
end
|
158
|
+
|
159
|
+
def call stream
|
160
|
+
each { |chunk| stream.write chunk }
|
161
|
+
ensure
|
162
|
+
close
|
163
|
+
end
|
129
164
|
end
|
130
165
|
end
|
data/lib/dial/prosopite.rb
CHANGED
@@ -4,5 +4,20 @@ require "logger"
|
|
4
4
|
|
5
5
|
module Dial
|
6
6
|
class ProsopiteLogger < Logger
|
7
|
+
def self.log_io
|
8
|
+
Thread.current[:dial_prosopite_log_io] ||= StringIO.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
super StringIO.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def add severity, message = nil, progname = nil
|
16
|
+
return if severity < level
|
17
|
+
|
18
|
+
progname = @progname if progname.nil?
|
19
|
+
formatted_message = format_message format_severity(severity), Time.now, progname, message
|
20
|
+
self.class.log_io.write formatted_message
|
21
|
+
end
|
7
22
|
end
|
8
23
|
end
|
data/lib/dial/railtie.rb
CHANGED
@@ -4,42 +4,32 @@ require "rails"
|
|
4
4
|
require "active_record"
|
5
5
|
require "prosopite"
|
6
6
|
|
7
|
-
require_relative "prosopite"
|
8
7
|
require_relative "middleware"
|
9
8
|
require_relative "prosopite_logger"
|
10
9
|
|
11
10
|
module Dial
|
12
11
|
class Railtie < ::Rails::Railtie
|
13
|
-
initializer "dial.
|
12
|
+
initializer "dial.setup", after: :load_config_initializers do |app|
|
13
|
+
# use middleware
|
14
14
|
app.middleware.insert_before 0, Middleware
|
15
|
-
end
|
16
|
-
|
17
|
-
initializer "dial.set_up_vernier", after: :load_config_initializers do |app|
|
18
|
-
app.config.after_initialize do
|
19
|
-
FileUtils.mkdir_p ::Rails.root.join VERNIER_PROFILE_OUT_RELATIVE_DIRNAME
|
20
|
-
end
|
21
|
-
end
|
22
15
|
|
23
|
-
|
24
|
-
stale_files("#{profile_out_dir_pathname}
|
16
|
+
# clean up stale vernier profile output files
|
17
|
+
stale_files("#{profile_out_dir_pathname}/*" + VERNIER_PROFILE_OUT_FILE_EXTENSION).each do |profile_out_file|
|
25
18
|
File.delete profile_out_file rescue nil
|
26
19
|
end
|
27
|
-
end
|
28
20
|
|
29
|
-
initializer "dial.set_up_prosopite", after: :load_config_initializers do |app|
|
30
21
|
app.config.after_initialize do
|
22
|
+
# set up vernier
|
23
|
+
FileUtils.mkdir_p ::Rails.root.join VERNIER_PROFILE_OUT_RELATIVE_DIRNAME
|
24
|
+
|
25
|
+
# set up prosopite
|
31
26
|
if ::ActiveRecord::Base.configurations.configurations.any? { |config| config.adapter == "postgresql" }
|
32
27
|
require "pg_query"
|
33
28
|
end
|
29
|
+
::Prosopite.custom_logger = ProsopiteLogger.new
|
34
30
|
|
35
|
-
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
initializer "dial.setup", after: :load_config_initializers do |app|
|
40
|
-
app.config.after_initialize do
|
31
|
+
# finalize configuration
|
41
32
|
Dial._configuration.freeze
|
42
|
-
|
43
33
|
::Prosopite.ignore_queries = Dial._configuration.prosopite_ignore_queries
|
44
34
|
end
|
45
35
|
end
|
data/lib/dial/version.rb
CHANGED
data/lib/dial.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dial
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joshua Young
|
@@ -138,7 +138,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
138
138
|
- !ruby/object:Gem::Version
|
139
139
|
version: '0'
|
140
140
|
requirements: []
|
141
|
-
rubygems_version: 3.
|
141
|
+
rubygems_version: 3.7.1
|
142
142
|
specification_version: 4
|
143
143
|
summary: A modern profiler for your Rails application
|
144
144
|
test_files: []
|