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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dc0b70ff1d770bd0fc85123280a07bb683d649a059df325b2b9cad938ea346e2
4
- data.tar.gz: ef4ab7cdb79f4e773b9d19d35d40624085a1ea15b47798fc9a092c476e4d5f25
3
+ metadata.gz: 204280e63f9f71fe949ccc5497b5e12e207ae681314caf73063163f785b7126b
4
+ data.tar.gz: 21c70e61114ef47f38e194b812c3b0f52aba68ba7976972b13700db90e1451a8
5
5
  SHA512:
6
- metadata.gz: 5fd19ba186d0689ad63827557f944c0fcee39c41c0d77053450d1a81ff52bb74442f26e7eb4bb377efacea763f87903d467c858834da56483a3d4ddf01307acc
7
- data.tar.gz: 45ccf2409afdc88cca32063aa3583737f00eaffea57f10701a9d6d2c45d066586c85e52eea2387ee9cacb2bc0712feca2159c16e711c52c1c8e2c4f8e34ef162
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
- 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:
@@ -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
- | 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.
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.
@@ -12,8 +12,10 @@ 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
+ 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
@@ -16,7 +16,10 @@ 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
+ 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
@@ -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
@@ -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
- panel_html = Panel.html env, headers, profile_out_filename, query_logs, ruby_vm_stat, gc_stat, gc_stat_heap, server_timing
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 write_vernier_result! result, pathname
79
- Thread.new do
80
- Thread.current.name = "Dial::Middleware#write_vernier_result!"
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
- result.write out: pathname
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
- # 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
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
- 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
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
@@ -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.0"
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.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.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: []