dial 0.3.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1cf2ace9f735e0ffd4c290b742bbf7f89f322babb96636e942fe1faaf92a8e45
4
- data.tar.gz: '06279e4a580959080df277abe1a8488bca20f50276d9221ca399366f570e1426'
3
+ metadata.gz: dc0b70ff1d770bd0fc85123280a07bb683d649a059df325b2b9cad938ea346e2
4
+ data.tar.gz: ef4ab7cdb79f4e773b9d19d35d40624085a1ea15b47798fc9a092c476e4d5f25
5
5
  SHA512:
6
- metadata.gz: bde524497181a25dbeddc54f8f04ff128714844a5f2b6d8c84ae569bf4d45cb39942dddbe0d687e63c7b3c23f590410128de4abfffa69e5ee41923be4a73b6c1
7
- data.tar.gz: e2bd5aef0ee0975a8e9c227a77296f40811d558251035771177c11f2128b599d905644186237280b3718f64ff65b31b834e01cfd222e5baba0077a53ee631451
6
+ metadata.gz: 5fd19ba186d0689ad63827557f944c0fcee39c41c0d77053450d1a81ff52bb74442f26e7eb4bb377efacea763f87903d467c858834da56483a3d4ddf01307acc
7
+ data.tar.gz: 45ccf2409afdc88cca32063aa3583737f00eaffea57f10701a9d6d2c45d066586c85e52eea2387ee9cacb2bc0712feca2159c16e711c52c1c8e2c4f8e34ef162
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
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
+
3
11
  ## [0.3.2] - 2025-05-14
4
12
 
5
13
  - Consolidate railtie initializers
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 (WIP) |
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
@@ -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|
@@ -25,5 +25,4 @@ module Dial
25
25
  VERNIER_VIEWER_URL = "https://vernier.prof"
26
26
 
27
27
  PROSOPITE_IGNORE_QUERIES = [/schema_migrations/i].freeze
28
- PROSOPITE_LOG_IO = StringIO.new
29
28
  end
@@ -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 + VERNIER_PROFILE_OUT_FILE_EXTENSION)
8
7
 
9
- if File.exist? path
10
- [
11
- 200,
12
- { "Content-Type" => "application/json", "Access-Control-Allow-Origin" => Dial::VERNIER_VIEWER_URL },
13
- [File.read(path)]
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,
@@ -22,6 +22,10 @@ module Dial
22
22
  return @app.call env
23
23
  end
24
24
 
25
+ unless should_profile?
26
+ return @app.call env
27
+ end
28
+
25
29
  start_time = Process.clock_gettime Process::CLOCK_MONOTONIC
26
30
 
27
31
  profile_out_filename = "#{Util.uuid}_vernier" + VERNIER_PROFILE_OUT_FILE_EXTENSION
@@ -49,19 +53,12 @@ module Dial
49
53
  finish_time = Process.clock_gettime Process::CLOCK_MONOTONIC
50
54
  env[REQUEST_TIMING] = ((finish_time - start_time) * 1_000).round 2
51
55
 
52
- body = String.new.tap do |str|
53
- rack_body.each { |chunk| str << chunk }
54
- rack_body.close if rack_body.respond_to? :close
55
-
56
- str.sub! "</body>", <<~HTML
57
- #{Panel.html env, headers, profile_out_filename, query_logs, ruby_vm_stat, gc_stat, gc_stat_heap, server_timing}
58
- </body>
59
- HTML
60
- 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
61
58
 
62
- headers[CONTENT_LENGTH] = body.bytesize.to_s
59
+ headers.delete CONTENT_LENGTH
63
60
 
64
- [status, headers, [body]]
61
+ [status, headers, body]
65
62
  end
66
63
 
67
64
  private
@@ -90,13 +87,13 @@ module Dial
90
87
  def clear_query_logs!
91
88
  [].tap do |query_logs|
92
89
  entry = section = count = nil
93
- PROSOPITE_LOG_IO.string.lines.each do |line|
90
+ ProsopiteLogger.log_io.string.lines.each do |line|
94
91
  entry, section, count = process_query_log_line line, entry, section, count
95
92
  query_logs << entry if entry && section.nil?
96
93
  end
97
94
 
98
- PROSOPITE_LOG_IO.truncate 0
99
- PROSOPITE_LOG_IO.rewind
95
+ ProsopiteLogger.log_io.truncate 0
96
+ ProsopiteLogger.log_io.rewind
100
97
  end
101
98
  end
102
99
 
@@ -127,5 +124,42 @@ module Dial
127
124
  def profile_out_dir_pathname
128
125
  ::Rails.root.join VERNIER_PROFILE_OUT_RELATIVE_DIRNAME
129
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
130
164
  end
131
165
  end
@@ -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
@@ -26,7 +26,7 @@ module Dial
26
26
  if ::ActiveRecord::Base.configurations.configurations.any? { |config| config.adapter == "postgresql" }
27
27
  require "pg_query"
28
28
  end
29
- ::Prosopite.custom_logger = ProsopiteLogger.new PROSOPITE_LOG_IO
29
+ ::Prosopite.custom_logger = ProsopiteLogger.new
30
30
 
31
31
  # finalize configuration
32
32
  Dial._configuration.freeze
data/lib/dial/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dial
4
- VERSION = "0.3.2"
4
+ VERSION = "0.4.0"
5
5
  end
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.3.2
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.6.9
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: []