dial 0.4.0 → 0.5.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 +8 -4
- data/README.md +61 -10
- data/lib/dial/configuration.rb +14 -2
- data/lib/dial/constants.rb +4 -1
- data/lib/dial/engine/routes.rb +15 -15
- data/lib/dial/middleware/panel.rb +4 -5
- data/lib/dial/middleware.rb +16 -16
- data/lib/dial/railtie.rb +5 -21
- data/lib/dial/storage/file_adapter.rb +80 -0
- data/lib/dial/storage/memcached_adapter.rb +27 -0
- data/lib/dial/storage/redis_adapter.rb +27 -0
- data/lib/dial/storage.rb +76 -0
- data/lib/dial/version.rb +1 -1
- data/lib/dial.rb +1 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 204280e63f9f71fe949ccc5497b5e12e207ae681314caf73063163f785b7126b
|
4
|
+
data.tar.gz: 21c70e61114ef47f38e194b812c3b0f52aba68ba7976972b13700db90e1451a8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e94c02266b0abd60727413d95bf7ec6238466dfa03725c904dac9443d1be63c448e05e7099d2c5ea44f7c153b155f8dd6176ca972103a0a3778bd8d7076b2f6e
|
7
|
+
data.tar.gz: a6207c9decc7cdd7c009e6d85b38745a53c49b9703740ea35c143ebbbe9f347784136c718f902de4050e9f7dbc02d9b339b3f0a3e28332db3d43c1105d25191a
|
data/CHANGELOG.md
CHANGED
@@ -1,12 +1,16 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.5.0] - 2025-09-21
|
4
|
+
|
5
|
+
- Add storage interface with Redis cluster and Memcached adapters for distributed deployments
|
6
|
+
|
3
7
|
## [0.4.0] - 2025-08-30
|
4
8
|
|
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
9
|
- Improve error handling and input validation
|
10
|
+
- Fix missing namespace for VERNIER_PROFILE_OUT_FILE_EXTENSION in engine routes
|
11
|
+
- Make prosopite logging thread-safe with thread-local storage
|
12
|
+
- Replace response body buffering with streaming response wrapper
|
13
|
+
- Add sampling configuration to control percentage of requests profiled
|
10
14
|
|
11
15
|
## [0.3.2] - 2025-05-14
|
12
16
|
|
data/README.md
CHANGED
@@ -19,9 +19,7 @@ Utilizes [vernier](https://github.com/jhawthorn/vernier) for profiling and
|
|
19
19
|
1. Add the gem to your Rails application's Gemfile:
|
20
20
|
|
21
21
|
```ruby
|
22
|
-
|
23
|
-
gem "dial"
|
24
|
-
end
|
22
|
+
gem "dial"
|
25
23
|
```
|
26
24
|
|
27
25
|
2. Install the gem:
|
@@ -34,7 +32,7 @@ bundle install
|
|
34
32
|
|
35
33
|
```ruby
|
36
34
|
# this will mount the engine at /dial
|
37
|
-
mount Dial::Engine, at: "/"
|
35
|
+
mount Dial::Engine, at: "/"
|
38
36
|
```
|
39
37
|
|
40
38
|
4. (Optional) Configure the gem in an initializer:
|
@@ -44,6 +42,8 @@ mount Dial::Engine, at: "/" if Rails.env.development?
|
|
44
42
|
|
45
43
|
Dial.configure do |config|
|
46
44
|
config.sampling_percentage = 50
|
45
|
+
config.storage = Dial::Storage::RedisAdapter
|
46
|
+
config.storage_options = { client: Redis.new(url: ENV["REDIS_URL"]), ttl: 86400 }
|
47
47
|
config.vernier_interval = 100
|
48
48
|
config.vernier_allocation_interval = 10_000
|
49
49
|
config.prosopite_ignore_queries += [/pg_sleep/i]
|
@@ -55,11 +55,62 @@ end
|
|
55
55
|
Option | Description | Default
|
56
56
|
:- | :- | :-
|
57
57
|
`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 }`
|
58
60
|
`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`
|
59
61
|
`vernier_interval` | Sets the `interval` option for vernier. | `200`
|
60
62
|
`vernier_allocation_interval` | Sets the `allocation_interval` option for vernier. | `2_000`
|
61
63
|
`prosopite_ignore_queries` | Sets the `ignore_queries` option for prosopite. | `[/schema_migrations/i]`
|
62
64
|
|
65
|
+
## Storage Backends
|
66
|
+
|
67
|
+
### File Storage (Default)
|
68
|
+
|
69
|
+
Profile data is stored as files on disk with polled expiration. Only suitable for development and single-server deployments.
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
Dial.configure do |config|
|
73
|
+
config.storage = Dial::Storage::FileAdapter
|
74
|
+
config.storage_options = { ttl: 86400 }
|
75
|
+
end
|
76
|
+
```
|
77
|
+
|
78
|
+
### Redis Storage
|
79
|
+
|
80
|
+
Profile data is stored in Redis with automatic expiration. Supports both single Redis instances and Redis Cluster.
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
# Single Redis instance
|
84
|
+
Dial.configure do |config|
|
85
|
+
config.storage = Dial::Storage::RedisAdapter
|
86
|
+
config.storage_options = { client: Redis.new(url: "redis://localhost:6379"), ttl: 86400 }
|
87
|
+
end
|
88
|
+
|
89
|
+
# Redis Cluster
|
90
|
+
Dial.configure do |config|
|
91
|
+
config.storage = Dial::Storage::RedisAdapter
|
92
|
+
config.storage_options = {
|
93
|
+
client: Redis::Cluster.new(nodes: [
|
94
|
+
"redis://node1:7000",
|
95
|
+
"redis://node2:7001",
|
96
|
+
"redis://node3:7002"
|
97
|
+
]),
|
98
|
+
ttl: 86400
|
99
|
+
}
|
100
|
+
end
|
101
|
+
```
|
102
|
+
|
103
|
+
### Memcached Storage
|
104
|
+
|
105
|
+
Profile data is stored in Memcached with automatic expiration.
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
Dial.configure do |config|
|
109
|
+
config.storage = Dial::Storage::MemcachedAdapter
|
110
|
+
config.storage_options = { client: Dalli::Client.new("localhost:11211"), ttl: 86400 }
|
111
|
+
end
|
112
|
+
```
|
113
|
+
|
63
114
|
## Comparison with [rack-mini-profiler](https://github.com/MiniProfiler/rack-mini-profiler)
|
64
115
|
|
65
116
|
| | rack-mini-profiler | Dial |
|
@@ -72,18 +123,18 @@ Option | Description | Default
|
|
72
123
|
| Memory Profiling | Yes (with memory_profiler) | Yes (*overall usage only) (via vernier hook - graph) |
|
73
124
|
| View Profiling | Yes | Yes (via vernier hook - marker table, chart) |
|
74
125
|
| Snapshot Sampling | Yes | No |
|
75
|
-
|
|
76
|
-
|
77
|
-
> [!NOTE]
|
78
|
-
> SQL queries displayed in the profile are not annotated with the caller location by default. If you're not using the
|
79
|
-
> [marginalia](https://github.com/basecamp/marginalia) gem to annotate your queries, you will need to extend your
|
80
|
-
> application's [ActiveRecord QueryLogs](https://edgeapi.rubyonrails.org/classes/ActiveRecord/QueryLogs.html) yourself.
|
126
|
+
| Storage Backends | Redis, Memcached, File, Memory | Redis, Memcached, File |
|
127
|
+
| Production Ready | Yes | Yes |
|
81
128
|
|
82
129
|
## Development
|
83
130
|
|
84
131
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake test` to run the
|
85
132
|
tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
86
133
|
|
134
|
+
### Testing Storage Adapters
|
135
|
+
|
136
|
+
To test the Redis and Memcached storage adapters, you'll need running instances: `docker compose -f docker-compose.storage.yml up`
|
137
|
+
|
87
138
|
## Contributing
|
88
139
|
|
89
140
|
Bug reports and pull requests are welcome on GitHub at https://github.com/joshuay03/dial.
|
data/lib/dial/configuration.rb
CHANGED
@@ -12,8 +12,10 @@ module Dial
|
|
12
12
|
class Configuration
|
13
13
|
def initialize
|
14
14
|
@options = {
|
15
|
-
sampling_percentage:
|
16
|
-
|
15
|
+
sampling_percentage: default_sampling_percentage,
|
16
|
+
storage: default_storage,
|
17
|
+
storage_options: { ttl: STORAGE_TTL },
|
18
|
+
content_security_policy_nonce: -> env, _headers { env[NONCE] || EMPTY_NONCE },
|
17
19
|
vernier_interval: VERNIER_INTERVAL,
|
18
20
|
vernier_allocation_interval: VERNIER_ALLOCATION_INTERVAL,
|
19
21
|
prosopite_ignore_queries: PROSOPITE_IGNORE_QUERIES,
|
@@ -35,5 +37,15 @@ module Dial
|
|
35
37
|
|
36
38
|
super
|
37
39
|
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
|
38
50
|
end
|
39
51
|
end
|
data/lib/dial/constants.rb
CHANGED
@@ -16,7 +16,10 @@ module Dial
|
|
16
16
|
NONCE = ::ActionDispatch::ContentSecurityPolicy::Request::NONCE
|
17
17
|
REQUEST_TIMING = "dial_request_timing"
|
18
18
|
|
19
|
-
|
19
|
+
SAMPLING_PERCENTAGE_DEV = 100
|
20
|
+
SAMPLING_PERCENTAGE_PROD = 1
|
21
|
+
STORAGE_TTL = 60 * 60
|
22
|
+
EMPTY_NONCE = ""
|
20
23
|
|
21
24
|
VERNIER_INTERVAL = 200
|
22
25
|
VERNIER_ALLOCATION_INTERVAL = 2_000
|
data/lib/dial/engine/routes.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "uri"
|
4
|
+
|
3
5
|
Dial::Engine.routes.draw do
|
4
6
|
scope path: "/dial", as: "dial" do
|
5
7
|
get "profile", to: lambda { |env|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
unless uuid.match?(/\A[0-9a-f-]+_vernier\z/)
|
8
|
+
query_params = URI.decode_www_form(env[::Rack::QUERY_STRING]).to_h
|
9
|
+
profile_key = query_params["key"]
|
10
|
+
unless profile_key && profile_key.match?(/\A[0-9a-f-]+_vernier\z/i)
|
10
11
|
return [
|
11
12
|
400,
|
12
13
|
{ "Content-Type" => "text/plain" },
|
@@ -14,28 +15,27 @@ Dial::Engine.routes.draw do
|
|
14
15
|
]
|
15
16
|
end
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
content = File.read path
|
18
|
+
profile_storage_key = Dial::Storage.profile_storage_key profile_key
|
19
|
+
begin
|
20
|
+
content = Dial::Storage.fetch profile_storage_key
|
21
|
+
if content
|
22
22
|
[
|
23
23
|
200,
|
24
24
|
{ "Content-Type" => "application/json", "Access-Control-Allow-Origin" => Dial::VERNIER_VIEWER_URL },
|
25
25
|
[content]
|
26
26
|
]
|
27
|
-
|
27
|
+
else
|
28
28
|
[
|
29
|
-
|
29
|
+
404,
|
30
30
|
{ "Content-Type" => "text/plain" },
|
31
|
-
["
|
31
|
+
["Not Found"]
|
32
32
|
]
|
33
33
|
end
|
34
|
-
|
34
|
+
rescue
|
35
35
|
[
|
36
|
-
|
36
|
+
500,
|
37
37
|
{ "Content-Type" => "text/plain" },
|
38
|
-
["
|
38
|
+
["Internal Server Error"]
|
39
39
|
]
|
40
40
|
end
|
41
41
|
}
|
@@ -7,7 +7,7 @@ module Dial
|
|
7
7
|
QUERY_CHARS_TRUNCATION_THRESHOLD = 100
|
8
8
|
|
9
9
|
class << self
|
10
|
-
def html env, headers,
|
10
|
+
def html env, headers, profile_key, query_logs, ruby_vm_stat, gc_stat, gc_stat_heap, server_timing
|
11
11
|
<<~HTML
|
12
12
|
<style>#{style}</style>
|
13
13
|
|
@@ -16,7 +16,7 @@ module Dial
|
|
16
16
|
<span id="dial-preview-header">
|
17
17
|
#{formatted_rails_route_info env} |
|
18
18
|
#{formatted_request_timing env} |
|
19
|
-
#{formatted_profile_output env,
|
19
|
+
#{formatted_profile_output env, profile_key}
|
20
20
|
</span>
|
21
21
|
<span id="dial-preview-rails-version">#{formatted_rails_version}</span>
|
22
22
|
<span id="dial-preview-rack-version">#{formatted_rack_version}</span>
|
@@ -189,11 +189,10 @@ module Dial
|
|
189
189
|
"<b>Request timing:</b> #{env[REQUEST_TIMING]}ms"
|
190
190
|
end
|
191
191
|
|
192
|
-
def formatted_profile_output env,
|
192
|
+
def formatted_profile_output env, profile_key
|
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
|
-
|
196
|
-
profile_out_url = URI.encode_www_form_component url_base + "#{prefix}dial/profile?uuid=#{uuid}"
|
195
|
+
profile_out_url = URI.encode_www_form_component url_base + "#{prefix}dial/profile?key=#{profile_key}"
|
197
196
|
|
198
197
|
"<a href='https://vernier.prof/from-url/#{profile_out_url}' target='_blank'>View profile</a>"
|
199
198
|
end
|
data/lib/dial/middleware.rb
CHANGED
@@ -28,9 +28,6 @@ module Dial
|
|
28
28
|
|
29
29
|
start_time = Process.clock_gettime Process::CLOCK_MONOTONIC
|
30
30
|
|
31
|
-
profile_out_filename = "#{Util.uuid}_vernier" + VERNIER_PROFILE_OUT_FILE_EXTENSION
|
32
|
-
profile_out_pathname = "#{profile_out_dir_pathname}/#{profile_out_filename}"
|
33
|
-
|
34
31
|
status, headers, rack_body, ruby_vm_stat, gc_stat, gc_stat_heap, vernier_result = nil
|
35
32
|
::Prosopite.scan do
|
36
33
|
vernier_result = ::Vernier.profile interval: Dial._configuration.vernier_interval, \
|
@@ -41,19 +38,19 @@ module Dial
|
|
41
38
|
end
|
42
39
|
end
|
43
40
|
end
|
44
|
-
server_timing = server_timing headers
|
45
41
|
|
46
42
|
unless headers[CONTENT_TYPE]&.include? CONTENT_TYPE_HTML
|
47
43
|
return [status, headers, rack_body]
|
48
44
|
end
|
49
45
|
|
50
|
-
write_vernier_result! vernier_result, profile_out_pathname
|
51
|
-
query_logs = clear_query_logs!
|
52
|
-
|
53
46
|
finish_time = Process.clock_gettime Process::CLOCK_MONOTONIC
|
54
47
|
env[REQUEST_TIMING] = ((finish_time - start_time) * 1_000).round 2
|
55
48
|
|
56
|
-
|
49
|
+
profile_key = Storage.generate_profile_key
|
50
|
+
store_profile_data! vernier_result, (Storage.profile_storage_key profile_key)
|
51
|
+
query_logs = clear_query_logs!
|
52
|
+
server_timing = server_timing headers
|
53
|
+
panel_html = Panel.html env, headers, profile_key, query_logs, ruby_vm_stat, gc_stat, gc_stat_heap, server_timing
|
57
54
|
body = PanelInjector.new rack_body, panel_html
|
58
55
|
|
59
56
|
headers.delete CONTENT_LENGTH
|
@@ -75,12 +72,19 @@ module Dial
|
|
75
72
|
]
|
76
73
|
end
|
77
74
|
|
78
|
-
def
|
79
|
-
Thread.new do
|
80
|
-
Thread.current.name = "Dial::Middleware#
|
75
|
+
def store_profile_data! vernier_result, profile_storage_key
|
76
|
+
Thread.new(vernier_result, profile_storage_key) do |vernier_result, profile_storage_key|
|
77
|
+
Thread.current.name = "Dial::Middleware#store_profile_data!"
|
81
78
|
Thread.current.report_on_exception = false
|
82
79
|
|
83
|
-
|
80
|
+
# TODO: Support StringIO in vernier's #write method to avoid temp file I/O
|
81
|
+
Tempfile.create(["vernier_profile", ".json"]) do |temp_file|
|
82
|
+
vernier_result.write out: temp_file.path
|
83
|
+
profile_data = File.read(temp_file.path)
|
84
|
+
Storage.store profile_storage_key, profile_data
|
85
|
+
end
|
86
|
+
|
87
|
+
Storage.cleanup
|
84
88
|
end
|
85
89
|
end
|
86
90
|
|
@@ -121,10 +125,6 @@ module Dial
|
|
121
125
|
end
|
122
126
|
end
|
123
127
|
|
124
|
-
def profile_out_dir_pathname
|
125
|
-
::Rails.root.join VERNIER_PROFILE_OUT_RELATIVE_DIRNAME
|
126
|
-
end
|
127
|
-
|
128
128
|
def should_profile?
|
129
129
|
rand(100) < Dial._configuration.sampling_percentage
|
130
130
|
end
|
data/lib/dial/railtie.rb
CHANGED
@@ -10,18 +10,10 @@ require_relative "prosopite_logger"
|
|
10
10
|
module Dial
|
11
11
|
class Railtie < ::Rails::Railtie
|
12
12
|
initializer "dial.setup", after: :load_config_initializers do |app|
|
13
|
-
#
|
14
|
-
|
15
|
-
|
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|
|
18
|
-
File.delete profile_out_file rescue nil
|
19
|
-
end
|
13
|
+
# clean up stale storage data
|
14
|
+
Storage.cleanup
|
20
15
|
|
21
16
|
app.config.after_initialize do
|
22
|
-
# set up vernier
|
23
|
-
FileUtils.mkdir_p ::Rails.root.join VERNIER_PROFILE_OUT_RELATIVE_DIRNAME
|
24
|
-
|
25
17
|
# set up prosopite
|
26
18
|
if ::ActiveRecord::Base.configurations.configurations.any? { |config| config.adapter == "postgresql" }
|
27
19
|
require "pg_query"
|
@@ -34,17 +26,9 @@ module Dial
|
|
34
26
|
end
|
35
27
|
end
|
36
28
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
Dir.glob(glob_pattern).select do |file|
|
41
|
-
timestamp = Util.uuid_timestamp Util.file_name_uuid File.basename file
|
42
|
-
timestamp < Time.now - FILE_STALE_SECONDS
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def profile_out_dir_pathname
|
47
|
-
::Rails.root.join VERNIER_PROFILE_OUT_RELATIVE_DIRNAME
|
29
|
+
initializer "dial.middleware", before: :build_middleware_stack do |app|
|
30
|
+
# use middleware
|
31
|
+
app.middleware.insert_before 0, Middleware
|
48
32
|
end
|
49
33
|
end
|
50
34
|
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "zlib"
|
4
|
+
require "fileutils"
|
5
|
+
|
6
|
+
module Dial
|
7
|
+
class Storage
|
8
|
+
class FileAdapter
|
9
|
+
def initialize options = {}
|
10
|
+
@ttl = options[:ttl] || STORAGE_TTL
|
11
|
+
@profile_dir = ::Rails.root.join VERNIER_PROFILE_OUT_RELATIVE_DIRNAME
|
12
|
+
FileUtils.mkdir_p @profile_dir
|
13
|
+
rescue Errno::ENOENT
|
14
|
+
FileUtils.mkdir_p File.dirname @profile_dir
|
15
|
+
FileUtils.mkdir_p @profile_dir
|
16
|
+
end
|
17
|
+
|
18
|
+
def store key, data, ttl: nil
|
19
|
+
ttl ||= @ttl
|
20
|
+
store_profile key, data, ttl
|
21
|
+
end
|
22
|
+
|
23
|
+
def fetch key
|
24
|
+
fetch_profile key
|
25
|
+
end
|
26
|
+
|
27
|
+
def delete key
|
28
|
+
delete_profile key
|
29
|
+
end
|
30
|
+
|
31
|
+
def cleanup
|
32
|
+
expired_files("#{@profile_dir}/*").each { |file| File.delete file rescue nil }
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def store_profile key, data, ttl
|
38
|
+
uuid = extract_uuid key
|
39
|
+
path = profile_path uuid
|
40
|
+
File.binwrite path, data
|
41
|
+
set_file_expiry path, ttl
|
42
|
+
end
|
43
|
+
|
44
|
+
def fetch_profile key
|
45
|
+
uuid = extract_uuid key
|
46
|
+
path = profile_path uuid
|
47
|
+
File.binread path if File.exist? path
|
48
|
+
end
|
49
|
+
|
50
|
+
def delete_profile key
|
51
|
+
uuid = extract_uuid key
|
52
|
+
path = profile_path uuid
|
53
|
+
File.delete path if File.exist? path
|
54
|
+
end
|
55
|
+
|
56
|
+
def extract_uuid key
|
57
|
+
Storage.extract_uuid key
|
58
|
+
end
|
59
|
+
|
60
|
+
def profile_path uuid
|
61
|
+
@profile_dir.join "#{uuid}#{VERNIER_PROFILE_OUT_FILE_EXTENSION}"
|
62
|
+
end
|
63
|
+
|
64
|
+
def expired_files glob_pattern
|
65
|
+
Dir.glob(glob_pattern).select { |file| file_expired? file }
|
66
|
+
end
|
67
|
+
|
68
|
+
def set_file_expiry path, ttl
|
69
|
+
expire_time = Time.now + ttl
|
70
|
+
File.utime expire_time, expire_time, path
|
71
|
+
end
|
72
|
+
|
73
|
+
def file_expired? path
|
74
|
+
return false unless File.exist? path
|
75
|
+
|
76
|
+
File.mtime(path) < Time.now
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dial
|
4
|
+
class Storage
|
5
|
+
class MemcachedAdapter
|
6
|
+
def initialize options = {}
|
7
|
+
raise ArgumentError, "Memcached client required" unless options[:client]
|
8
|
+
|
9
|
+
@client = options[:client]
|
10
|
+
@ttl = options[:ttl] || STORAGE_TTL
|
11
|
+
end
|
12
|
+
|
13
|
+
def store key, data, ttl: nil
|
14
|
+
ttl ||= @ttl
|
15
|
+
@client.set (Storage.format_key key), data, ttl
|
16
|
+
end
|
17
|
+
|
18
|
+
def fetch key
|
19
|
+
@client.get Storage.format_key key
|
20
|
+
end
|
21
|
+
|
22
|
+
def delete key
|
23
|
+
@client.delete Storage.format_key key
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dial
|
4
|
+
class Storage
|
5
|
+
class RedisAdapter
|
6
|
+
def initialize options = {}
|
7
|
+
raise ArgumentError, "Redis client required" unless options[:client]
|
8
|
+
|
9
|
+
@client = options[:client]
|
10
|
+
@ttl = options[:ttl] || STORAGE_TTL
|
11
|
+
end
|
12
|
+
|
13
|
+
def store key, data, ttl: nil
|
14
|
+
ttl ||= @ttl
|
15
|
+
@client.setex (Storage.format_key key), ttl, data
|
16
|
+
end
|
17
|
+
|
18
|
+
def fetch key
|
19
|
+
@client.get Storage.format_key key
|
20
|
+
end
|
21
|
+
|
22
|
+
def delete key
|
23
|
+
@client.del Storage.format_key key
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/dial/storage.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "storage/file_adapter"
|
4
|
+
require_relative "storage/redis_adapter"
|
5
|
+
require_relative "storage/memcached_adapter"
|
6
|
+
|
7
|
+
module Dial
|
8
|
+
class Storage
|
9
|
+
SUPPORTED_ADAPTERS = [FileAdapter, RedisAdapter, MemcachedAdapter].freeze
|
10
|
+
|
11
|
+
@mutex = Mutex.new
|
12
|
+
|
13
|
+
class << self
|
14
|
+
def validate_key! key
|
15
|
+
unless key.match?(/\A.+_vernier:.+\z/)
|
16
|
+
raise ArgumentError, "Invalid key format: #{key}. Expected format: '<uuid>_vernier:<suffix>'"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def extract_uuid key
|
21
|
+
validate_key! key
|
22
|
+
key.split(":", 2).first
|
23
|
+
end
|
24
|
+
|
25
|
+
# Format key for Redis Cluster compatibility (hash tags)
|
26
|
+
def format_key key
|
27
|
+
uuid = extract_uuid key
|
28
|
+
suffix = key.split(":", 2).last
|
29
|
+
"{#{uuid}}:#{suffix}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def profile_storage_key profile_key
|
33
|
+
"#{profile_key}:profile"
|
34
|
+
end
|
35
|
+
|
36
|
+
def generate_profile_key
|
37
|
+
"#{Util.uuid}_vernier"
|
38
|
+
end
|
39
|
+
|
40
|
+
def adapter
|
41
|
+
return @adapter if @adapter
|
42
|
+
|
43
|
+
@mutex.synchronize do
|
44
|
+
@adapter ||= build_adapter
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def store key, data, ttl: nil
|
49
|
+
adapter.store key, data, ttl: ttl
|
50
|
+
end
|
51
|
+
|
52
|
+
def fetch key
|
53
|
+
adapter.fetch key
|
54
|
+
end
|
55
|
+
|
56
|
+
def delete key
|
57
|
+
adapter.delete key
|
58
|
+
end
|
59
|
+
|
60
|
+
def cleanup
|
61
|
+
adapter.cleanup if adapter.respond_to? :cleanup
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def build_adapter
|
67
|
+
config = Dial._configuration
|
68
|
+
unless SUPPORTED_ADAPTERS.include? storage_class = config.storage
|
69
|
+
raise ArgumentError, "Unsupported storage type: #{storage_class}. Supported adapters: #{SUPPORTED_ADAPTERS.map(&:name).join ', '}"
|
70
|
+
end
|
71
|
+
|
72
|
+
storage_class.new config.storage_options
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
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.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joshua Young
|
@@ -116,6 +116,10 @@ files:
|
|
116
116
|
- lib/dial/prosopite.rb
|
117
117
|
- lib/dial/prosopite_logger.rb
|
118
118
|
- lib/dial/railtie.rb
|
119
|
+
- lib/dial/storage.rb
|
120
|
+
- lib/dial/storage/file_adapter.rb
|
121
|
+
- lib/dial/storage/memcached_adapter.rb
|
122
|
+
- lib/dial/storage/redis_adapter.rb
|
119
123
|
- lib/dial/util.rb
|
120
124
|
- lib/dial/version.rb
|
121
125
|
homepage: https://github.com/joshuay03/dial
|
@@ -138,7 +142,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
138
142
|
- !ruby/object:Gem::Version
|
139
143
|
version: '0'
|
140
144
|
requirements: []
|
141
|
-
rubygems_version: 3.7.
|
145
|
+
rubygems_version: 3.7.2
|
142
146
|
specification_version: 4
|
143
147
|
summary: A modern profiler for your Rails application
|
144
148
|
test_files: []
|