Skip to content

Commit f6ffd4b

Browse files
committed
add enabled configuration option and force parameter for per-request profiling
1 parent c0cdd0c commit f6ffd4b

File tree

7 files changed

+106
-21
lines changed

7 files changed

+106
-21
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
## [Unreleased]
22

33
- Don't clean up stale storage data in railtie
4+
- Add enabled configuration option and force parameter for per-request profiling
45

56
## [0.5.0] - 2025-09-21
67

README.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,15 @@ mount Dial::Engine, at: "/"
4141
# config/initializers/dial.rb
4242

4343
Dial.configure do |config|
44-
config.sampling_percentage = 50
45-
config.storage = Dial::Storage::RedisAdapter
46-
config.storage_options = { client: Redis.new(url: ENV["REDIS_URL"]), ttl: 86400 }
44+
config.enabled = !Rails.env.production? # disable by default in production, use force_param to enable per request
45+
config.force_param = "profile" # override param name to force profiling
46+
if Rails.env.staging?
47+
config.sampling_percentage = 50 # override sampling percentage in staging for A/B testing profiler impact
48+
end
49+
unless Rails.env.development?
50+
config.storage = Dial::Storage::RedisAdapter # use Redis storage in non-development environments
51+
config.storage_options = { client: Redis.new(url: ENV["REDIS_URL"]), ttl: 86400 }
52+
end
4753
config.vernier_interval = 100
4854
config.vernier_allocation_interval = 10_000
4955
config.prosopite_ignore_queries += [/pg_sleep/i]
@@ -54,9 +60,11 @@ end
5460

5561
Option | Description | Default
5662
:- | :- | :-
63+
`enabled` | Whether profiling is enabled. | `true`
64+
`force_param` | Request parameter name to force profiling even when disabled. Always profiles (bypasses sampling). | `"dial_force"`
5765
`sampling_percentage` | Percentage of requests to profile. | `100` in development, `1` in production
58-
`storage` | Storage adapter class for profile data | `Dial::Storage::FileAdapter`
59-
`storage_options` | Options hash passed to storage adapter | `{ ttl: 3600 }`
66+
`storage` | Storage adapter class for profile data. | `Dial::Storage::FileAdapter`
67+
`storage_options` | Options hash passed to storage adapter. | `{ ttl: 3600 }`
6068
`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`
6169
`vernier_interval` | Sets the `interval` option for vernier. | `200`
6270
`vernier_allocation_interval` | Sets the `allocation_interval` option for vernier. | `2_000`

lib/dial/configuration.rb

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ def self._configuration
1212
class Configuration
1313
def initialize
1414
@options = {
15-
sampling_percentage: default_sampling_percentage,
16-
storage: default_storage,
15+
enabled: true,
16+
force_param: FORCE_PARAM,
17+
sampling_percentage: ::Rails.env.development? ? SAMPLING_PERCENTAGE_DEV : SAMPLING_PERCENTAGE_PROD,
18+
storage: Storage::FileAdapter,
1719
storage_options: { ttl: STORAGE_TTL },
1820
content_security_policy_nonce: -> env, _headers { env[NONCE] || EMPTY_NONCE },
1921
vernier_interval: VERNIER_INTERVAL,
@@ -37,15 +39,5 @@ def freeze
3739

3840
super
3941
end
40-
41-
private
42-
43-
def default_sampling_percentage
44-
::Rails.env.development? ? SAMPLING_PERCENTAGE_DEV : SAMPLING_PERCENTAGE_PROD
45-
end
46-
47-
def default_storage
48-
Storage::FileAdapter
49-
end
5042
end
5143
end

lib/dial/constants.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ module Dial
1616
NONCE = ::ActionDispatch::ContentSecurityPolicy::Request::NONCE
1717
REQUEST_TIMING = "dial_request_timing"
1818

19+
FORCE_PARAM = "dial_force"
1920
SAMPLING_PERCENTAGE_DEV = 100
2021
SAMPLING_PERCENTAGE_PROD = 1
2122
STORAGE_TTL = 60 * 60

lib/dial/middleware.rb

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ def call env
2222
return @app.call env
2323
end
2424

25-
unless should_profile?
25+
request = ::Rack::Request.new env
26+
unless should_profile? request
2627
return @app.call env
2728
end
2829

@@ -125,8 +126,12 @@ def process_query_log_line line, entry, section, count
125126
end
126127
end
127128

128-
def should_profile?
129-
rand(100) < Dial._configuration.sampling_percentage
129+
def should_profile? request
130+
force_param = Dial._configuration.force_param
131+
return true if request.params[force_param]
132+
133+
Dial._configuration.enabled &&
134+
rand(100) < Dial._configuration.sampling_percentage
130135
end
131136
end
132137

test/dial/test_configuration.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ def test_configure_yields_a_new_configuration
1212

1313
def test_configuration_has_default_values
1414
Dial.configure do |config|
15+
assert_equal true, config.enabled
16+
assert_equal FORCE_PARAM, config.force_param
1517
assert_equal 100, config.sampling_percentage
1618
assert_instance_of Proc, config.content_security_policy_nonce
1719
assert_equal VERNIER_INTERVAL, config.vernier_interval
@@ -24,17 +26,21 @@ def test_configuration_has_default_values
2426

2527
def test_configuration_can_be_changed
2628
Dial.configure do |config|
29+
config.enabled = false
30+
config.force_param = "profile"
2731
config.sampling_percentage = 25
2832
config.content_security_policy_nonce = lambda { |_env, headers| headers["TEST_NONCE"] }
2933
config.vernier_interval = 50
3034
config.vernier_allocation_interval = 100
3135
config.prosopite_ignore_queries = [/only_ignore_me/]
3236

37+
assert_equal false, config.enabled
38+
assert_equal "profile", config.force_param
3339
assert_equal 25, config.sampling_percentage
40+
assert_equal "test_nonce", config.content_security_policy_nonce.call({}, { "TEST_NONCE" => "test_nonce" })
3441
assert_equal 50, config.vernier_interval
3542
assert_equal 100, config.vernier_allocation_interval
3643
assert_equal [/only_ignore_me/], config.prosopite_ignore_queries
37-
assert_equal "test_nonce", config.content_security_policy_nonce.call({}, { "TEST_NONCE" => "test_nonce" })
3844
end
3945
end
4046
end
@@ -49,15 +55,21 @@ def teardown
4955
def test_configuration_can_be_changed
5056
config_initializer <<~RUBY
5157
Dial.configure do |config|
58+
config.enabled = false
59+
config.force_param = "profile"
5260
config.sampling_percentage = 25
61+
config.content_security_policy_nonce = lambda { |_env, headers| headers["TEST_NONCE"] }
5362
config.vernier_interval = 50
5463
config.vernier_allocation_interval = 100
5564
config.prosopite_ignore_queries = [/only_ignore_me/]
5665
end
5766
RUBY
5867
app(true).initialize!
5968

69+
assert_equal false, Dial._configuration.enabled
70+
assert_equal "profile", Dial._configuration.force_param
6071
assert_equal 25, Dial._configuration.sampling_percentage
72+
assert_equal "test_nonce", Dial._configuration.content_security_policy_nonce.call({}, { "TEST_NONCE" => "test_nonce" })
6173
assert_equal 50, Dial._configuration.vernier_interval
6274
assert_equal 100, Dial._configuration.vernier_allocation_interval
6375
assert_equal [/only_ignore_me/], Dial._configuration.prosopite_ignore_queries

test/dial/test_middleware.rb

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "../test_helper"
4+
5+
module Dial
6+
class TestMiddleware < Dial::Test
7+
def setup
8+
super
9+
@app = lambda { |env| [200, { "Content-Type" => "text/html" }, ["<html><body>Test</body></html>"]] }
10+
@middleware = Middleware.new @app
11+
@base_env = {
12+
"HTTP_ACCEPT" => "text/html",
13+
"REQUEST_METHOD" => "GET",
14+
"PATH_INFO" => "/test",
15+
"HTTP_HOST" => "example.com"
16+
}
17+
@base_request = ::Rack::Request.new @base_env
18+
end
19+
20+
def test_should_profile_when_enabled
21+
assert @middleware.send :should_profile?, @base_request
22+
end
23+
24+
def test_should_not_profile_when_not_within_sampling
25+
with_config sampling_percentage: 0 do
26+
refute @middleware.send :should_profile?, @base_request
27+
end
28+
end
29+
30+
def test_should_not_profile_when_disabled
31+
with_config enabled: false do
32+
refute @middleware.send :should_profile?, @base_request
33+
end
34+
end
35+
36+
def test_should_profile_when_disabled_and_forced
37+
with_config enabled: false, force_param: "profile" do
38+
env = @base_env.merge "QUERY_STRING" => "profile=1"
39+
request = ::Rack::Request.new env
40+
41+
assert @middleware.send :should_profile?, request
42+
end
43+
end
44+
45+
def test_should_profile_when_disabled_and_forced_and_not_within_sampling
46+
with_config enabled: false, force_param: "profile", sampling_percentage: 0 do
47+
env = @base_env.merge "QUERY_STRING" => "profile=1"
48+
request = ::Rack::Request.new env
49+
50+
assert @middleware.send :should_profile?, request
51+
end
52+
end
53+
54+
private
55+
56+
def with_config options = {}
57+
original_config = Dial._configuration.instance_variable_get :@options
58+
new_config = original_config.merge options
59+
60+
Dial._configuration.instance_variable_set :@options, new_config
61+
yield
62+
ensure
63+
Dial._configuration.instance_variable_set :@options, original_config
64+
end
65+
end
66+
end

0 commit comments

Comments
 (0)