dial 0.4.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dc0b70ff1d770bd0fc85123280a07bb683d649a059df325b2b9cad938ea346e2
4
- data.tar.gz: ef4ab7cdb79f4e773b9d19d35d40624085a1ea15b47798fc9a092c476e4d5f25
3
+ metadata.gz: d4f60345ab5528878505773aa1b5875d4e00164ac9e94968584a40ecbb18b251
4
+ data.tar.gz: 5d2d5399d37be584ed2dd17f39cd98ed45af05a66e2eb8609fd340ef54dbeeb9
5
5
  SHA512:
6
- metadata.gz: 5fd19ba186d0689ad63827557f944c0fcee39c41c0d77053450d1a81ff52bb74442f26e7eb4bb377efacea763f87903d467c858834da56483a3d4ddf01307acc
7
- data.tar.gz: 45ccf2409afdc88cca32063aa3583737f00eaffea57f10701a9d6d2c45d066586c85e52eea2387ee9cacb2bc0712feca2159c16e711c52c1c8e2c4f8e34ef162
6
+ metadata.gz: f78cf96c6482b571710c49363e00e7ffb5e65f8e07f180d9e5d99ce862af4d28c2032ff8757b565dc053699e0d4d044dfae26346de297b310c4324c659101216
7
+ data.tar.gz: 522ab5c9d3f3e556a71f440256a36d6f2bedc0abb5dccab05c147bc21a50332da581683188fcb1f672c58fc05773782d65fb575995d6adc0f0a856b9e78d6e8c
data/CHANGELOG.md CHANGED
@@ -1,12 +1,21 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.5.1] - 2025-09-27
4
+
5
+ - Don't clean up stale storage data in railtie
6
+ - Add enabled configuration option and force parameter for per-request profiling
7
+
8
+ ## [0.5.0] - 2025-09-21
9
+
10
+ - Add storage interface with Redis cluster and Memcached adapters for distributed deployments
11
+
3
12
  ## [0.4.0] - 2025-08-30
4
13
 
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
14
  - Improve error handling and input validation
15
+ - Fix missing namespace for VERNIER_PROFILE_OUT_FILE_EXTENSION in engine routes
16
+ - Make prosopite logging thread-safe with thread-local storage
17
+ - Replace response body buffering with streaming response wrapper
18
+ - Add sampling configuration to control percentage of requests profiled
10
19
 
11
20
  ## [0.3.2] - 2025-05-14
12
21
 
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
- group :development do
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: "/" if Rails.env.development?
35
+ mount Dial::Engine, at: "/"
38
36
  ```
39
37
 
40
38
  4. (Optional) Configure the gem in an initializer:
@@ -43,7 +41,15 @@ mount Dial::Engine, at: "/" if Rails.env.development?
43
41
  # config/initializers/dial.rb
44
42
 
45
43
  Dial.configure do |config|
46
- config.sampling_percentage = 50
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
47
53
  config.vernier_interval = 100
48
54
  config.vernier_allocation_interval = 10_000
49
55
  config.prosopite_ignore_queries += [/pg_sleep/i]
@@ -54,12 +60,65 @@ end
54
60
 
55
61
  Option | Description | Default
56
62
  :- | :- | :-
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"`
57
65
  `sampling_percentage` | Percentage of requests to profile. | `100` in development, `1` in production
66
+ `storage` | Storage adapter class for profile data. | `Dial::Storage::FileAdapter`
67
+ `storage_options` | Options hash passed to storage adapter. | `{ ttl: 3600 }`
58
68
  `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
69
  `vernier_interval` | Sets the `interval` option for vernier. | `200`
60
70
  `vernier_allocation_interval` | Sets the `allocation_interval` option for vernier. | `2_000`
61
71
  `prosopite_ignore_queries` | Sets the `ignore_queries` option for prosopite. | `[/schema_migrations/i]`
62
72
 
73
+ ## Storage Backends
74
+
75
+ ### File Storage (Default)
76
+
77
+ Profile data is stored as files on disk with polled expiration. Only suitable for development and single-server deployments.
78
+
79
+ ```ruby
80
+ Dial.configure do |config|
81
+ config.storage = Dial::Storage::FileAdapter
82
+ config.storage_options = { ttl: 86400 }
83
+ end
84
+ ```
85
+
86
+ ### Redis Storage
87
+
88
+ Profile data is stored in Redis with automatic expiration. Supports both single Redis instances and Redis Cluster.
89
+
90
+ ```ruby
91
+ # Single Redis instance
92
+ Dial.configure do |config|
93
+ config.storage = Dial::Storage::RedisAdapter
94
+ config.storage_options = { client: Redis.new(url: "redis://localhost:6379"), ttl: 86400 }
95
+ end
96
+
97
+ # Redis Cluster
98
+ Dial.configure do |config|
99
+ config.storage = Dial::Storage::RedisAdapter
100
+ config.storage_options = {
101
+ client: Redis::Cluster.new(nodes: [
102
+ "redis://node1:7000",
103
+ "redis://node2:7001",
104
+ "redis://node3:7002"
105
+ ]),
106
+ ttl: 86400
107
+ }
108
+ end
109
+ ```
110
+
111
+ ### Memcached Storage
112
+
113
+ Profile data is stored in Memcached with automatic expiration.
114
+
115
+ ```ruby
116
+ Dial.configure do |config|
117
+ config.storage = Dial::Storage::MemcachedAdapter
118
+ config.storage_options = { client: Dalli::Client.new("localhost:11211"), ttl: 86400 }
119
+ end
120
+ ```
121
+
63
122
  ## Comparison with [rack-mini-profiler](https://github.com/MiniProfiler/rack-mini-profiler)
64
123
 
65
124
  | | rack-mini-profiler | Dial |
@@ -72,18 +131,18 @@ Option | Description | Default
72
131
  | Memory Profiling | Yes (with memory_profiler) | Yes (*overall usage only) (via vernier hook - graph) |
73
132
  | View Profiling | Yes | Yes (via vernier hook - marker table, chart) |
74
133
  | Snapshot Sampling | Yes | No |
75
- | Production Support | Yes | No |
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.
134
+ | Storage Backends | Redis, Memcached, File, Memory | Redis, Memcached, File |
135
+ | Production Ready | Yes | Yes |
81
136
 
82
137
  ## Development
83
138
 
84
139
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake test` to run the
85
140
  tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
86
141
 
142
+ ### Testing Storage Adapters
143
+
144
+ To test the Redis and Memcached storage adapters, you'll need running instances: `docker compose -f docker-compose.storage.yml up`
145
+
87
146
  ## Contributing
88
147
 
89
148
  Bug reports and pull requests are welcome on GitHub at https://github.com/joshuay03/dial.
@@ -12,8 +12,12 @@ 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
+ enabled: true,
16
+ force_param: FORCE_PARAM,
17
+ sampling_percentage: ::Rails.env.development? ? SAMPLING_PERCENTAGE_DEV : SAMPLING_PERCENTAGE_PROD,
18
+ storage: Storage::FileAdapter,
19
+ storage_options: { ttl: STORAGE_TTL },
20
+ content_security_policy_nonce: -> env, _headers { env[NONCE] || EMPTY_NONCE },
17
21
  vernier_interval: VERNIER_INTERVAL,
18
22
  vernier_allocation_interval: VERNIER_ALLOCATION_INTERVAL,
19
23
  prosopite_ignore_queries: PROSOPITE_IGNORE_QUERIES,
@@ -16,7 +16,11 @@ module Dial
16
16
  NONCE = ::ActionDispatch::ContentSecurityPolicy::Request::NONCE
17
17
  REQUEST_TIMING = "dial_request_timing"
18
18
 
19
- FILE_STALE_SECONDS = 60 * 60
19
+ FORCE_PARAM = "dial_force"
20
+ SAMPLING_PERCENTAGE_DEV = 100
21
+ SAMPLING_PERCENTAGE_PROD = 1
22
+ STORAGE_TTL = 60 * 60
23
+ EMPTY_NONCE = ""
20
24
 
21
25
  VERNIER_INTERVAL = 200
22
26
  VERNIER_ALLOCATION_INTERVAL = 2_000
@@ -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
- uuid = env[::Rack::QUERY_STRING].sub "uuid=", ""
7
-
8
- # Validate UUID format (should end with _vernier)
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
- 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
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
- rescue
27
+ else
28
28
  [
29
- 500,
29
+ 404,
30
30
  { "Content-Type" => "text/plain" },
31
- ["Internal Server Error"]
31
+ ["Not Found"]
32
32
  ]
33
33
  end
34
- else
34
+ rescue
35
35
  [
36
- 404,
36
+ 500,
37
37
  { "Content-Type" => "text/plain" },
38
- ["Not Found"]
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, profile_out_filename, query_logs, ruby_vm_stat, gc_stat, gc_stat_heap, server_timing
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, profile_out_filename}
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, profile_out_filename
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
- uuid = profile_out_filename.delete_suffix VERNIER_PROFILE_OUT_FILE_EXTENSION
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
@@ -22,15 +22,13 @@ module Dial
22
22
  return @app.call env
23
23
  end
24
24
 
25
- unless should_profile?
25
+ request = ::Rack::Request.new env
26
+ unless should_profile? request
26
27
  return @app.call env
27
28
  end
28
29
 
29
30
  start_time = Process.clock_gettime Process::CLOCK_MONOTONIC
30
31
 
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
32
  status, headers, rack_body, ruby_vm_stat, gc_stat, gc_stat_heap, vernier_result = nil
35
33
  ::Prosopite.scan do
36
34
  vernier_result = ::Vernier.profile interval: Dial._configuration.vernier_interval, \
@@ -41,19 +39,19 @@ module Dial
41
39
  end
42
40
  end
43
41
  end
44
- server_timing = server_timing headers
45
42
 
46
43
  unless headers[CONTENT_TYPE]&.include? CONTENT_TYPE_HTML
47
44
  return [status, headers, rack_body]
48
45
  end
49
46
 
50
- write_vernier_result! vernier_result, profile_out_pathname
51
- query_logs = clear_query_logs!
52
-
53
47
  finish_time = Process.clock_gettime Process::CLOCK_MONOTONIC
54
48
  env[REQUEST_TIMING] = ((finish_time - start_time) * 1_000).round 2
55
49
 
56
- panel_html = Panel.html env, headers, profile_out_filename, query_logs, ruby_vm_stat, gc_stat, gc_stat_heap, server_timing
50
+ profile_key = Storage.generate_profile_key
51
+ store_profile_data! vernier_result, (Storage.profile_storage_key profile_key)
52
+ query_logs = clear_query_logs!
53
+ server_timing = server_timing headers
54
+ panel_html = Panel.html env, headers, profile_key, query_logs, ruby_vm_stat, gc_stat, gc_stat_heap, server_timing
57
55
  body = PanelInjector.new rack_body, panel_html
58
56
 
59
57
  headers.delete CONTENT_LENGTH
@@ -75,12 +73,19 @@ module Dial
75
73
  ]
76
74
  end
77
75
 
78
- def write_vernier_result! result, pathname
79
- Thread.new do
80
- Thread.current.name = "Dial::Middleware#write_vernier_result!"
76
+ def store_profile_data! vernier_result, profile_storage_key
77
+ Thread.new(vernier_result, profile_storage_key) do |vernier_result, profile_storage_key|
78
+ Thread.current.name = "Dial::Middleware#store_profile_data!"
81
79
  Thread.current.report_on_exception = false
82
80
 
83
- result.write out: pathname
81
+ # TODO: Support StringIO in vernier's #write method to avoid temp file I/O
82
+ Tempfile.create(["vernier_profile", ".json"]) do |temp_file|
83
+ vernier_result.write out: temp_file.path
84
+ profile_data = File.read(temp_file.path)
85
+ Storage.store profile_storage_key, profile_data
86
+ end
87
+
88
+ Storage.cleanup
84
89
  end
85
90
  end
86
91
 
@@ -121,12 +126,12 @@ module Dial
121
126
  end
122
127
  end
123
128
 
124
- def profile_out_dir_pathname
125
- ::Rails.root.join VERNIER_PROFILE_OUT_RELATIVE_DIRNAME
126
- end
129
+ def should_profile? request
130
+ force_param = Dial._configuration.force_param
131
+ return true if request.params[force_param]
127
132
 
128
- def should_profile?
129
- rand(100) < Dial._configuration.sampling_percentage
133
+ Dial._configuration.enabled &&
134
+ rand(100) < Dial._configuration.sampling_percentage
130
135
  end
131
136
  end
132
137
 
data/lib/dial/railtie.rb CHANGED
@@ -10,18 +10,7 @@ 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
- # use middleware
14
- app.middleware.insert_before 0, Middleware
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
20
-
21
13
  app.config.after_initialize do
22
- # set up vernier
23
- FileUtils.mkdir_p ::Rails.root.join VERNIER_PROFILE_OUT_RELATIVE_DIRNAME
24
-
25
14
  # set up prosopite
26
15
  if ::ActiveRecord::Base.configurations.configurations.any? { |config| config.adapter == "postgresql" }
27
16
  require "pg_query"
@@ -34,17 +23,9 @@ module Dial
34
23
  end
35
24
  end
36
25
 
37
- private
38
-
39
- def stale_files glob_pattern
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
26
+ initializer "dial.middleware", before: :build_middleware_stack do |app|
27
+ # use middleware
28
+ app.middleware.insert_before 0, Middleware
48
29
  end
49
30
  end
50
31
  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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dial
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.1"
5
5
  end
data/lib/dial.rb CHANGED
@@ -4,6 +4,7 @@ require_relative "dial/constants"
4
4
  require_relative "dial/util"
5
5
 
6
6
  require_relative "dial/configuration"
7
+ require_relative "dial/storage"
7
8
 
8
9
  require_relative "dial/railtie"
9
10
  require_relative "dial/engine"
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.0
4
+ version: 0.5.1
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.1
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: []