rage-rb 1.18.0 → 1.19.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.
@@ -51,18 +51,30 @@ require "logger"
51
51
  # end
52
52
  # ```
53
53
  class Rage::Logger
54
+ # @private
54
55
  METHODS_MAP = {
55
- "debug" => Logger::DEBUG,
56
- "info" => Logger::INFO,
57
- "warn" => Logger::WARN,
58
- "error" => Logger::ERROR,
59
- "fatal" => Logger::FATAL,
60
- "unknown" => Logger::UNKNOWN
56
+ debug: Logger::DEBUG,
57
+ info: Logger::INFO,
58
+ warn: Logger::WARN,
59
+ error: Logger::ERROR,
60
+ fatal: Logger::FATAL,
61
+ unknown: Logger::UNKNOWN
61
62
  }
62
- private_constant :METHODS_MAP
63
63
 
64
64
  attr_reader :level, :formatter
65
65
 
66
+ # @private
67
+ attr_reader :dynamic_tags, :dynamic_context
68
+
69
+ # @private
70
+ attr_reader :external_logger
71
+
72
+ # @private
73
+ module External
74
+ Static = Data.define(:wrapped)
75
+ Dynamic = Data.define(:wrapped)
76
+ end
77
+
66
78
  # Create a new logger.
67
79
  #
68
80
  # @param log [Object] a filename (`String`), IO object (typically `STDOUT`, `STDERR`, or an open file), `nil` (it writes nothing) or `File::NULL` (same as `nil`)
@@ -73,7 +85,10 @@ class Rage::Logger
73
85
  # @param shift_period_suffix [String] the log file suffix format for daily, weekly or monthly rotation
74
86
  # @param binmode sets whether the logger writes in binary mode
75
87
  def initialize(log, level: Logger::DEBUG, formatter: Rage::TextFormatter.new, shift_age: 0, shift_size: 104857600, shift_period_suffix: "%Y%m%d", binmode: false)
76
- @logdev = if log && log != File::NULL
88
+ @logdev = if log.class.name.start_with?("Rage::Logger::External::")
89
+ @external_logger = log
90
+ Logger::LogDevice.new(STDERR)
91
+ elsif log && log != File::NULL
77
92
  Logger::LogDevice.new(log, shift_age:, shift_size:, shift_period_suffix:, binmode:)
78
93
  end
79
94
 
@@ -83,17 +98,38 @@ class Rage::Logger
83
98
 
84
99
  @formatter = formatter
85
100
  @level = @logdev ? level : Logger::UNKNOWN
86
- define_log_methods
101
+ rebuild!
87
102
  end
88
103
 
104
+ # Set the logging severity threshold.
105
+ # @param level [Integer] logging severity threshold
89
106
  def level=(level)
90
107
  @level = level
91
- define_log_methods
108
+ rebuild!
92
109
  end
93
110
 
111
+ # Set the logging formatter.
112
+ # @param formatter [#call] logging formatter
94
113
  def formatter=(formatter)
95
114
  @formatter = formatter
96
- define_log_methods
115
+ rebuild!
116
+ end
117
+
118
+ # Write the given `msg` to the log with no formatting.
119
+ def <<(msg)
120
+ @logdev&.write(msg)
121
+ end
122
+
123
+ # @private
124
+ def dynamic_tags=(dynamic_tags)
125
+ @dynamic_tags = dynamic_tags
126
+ rebuild!
127
+ end
128
+
129
+ # @private
130
+ def dynamic_context=(dynamic_context)
131
+ @dynamic_context = dynamic_context
132
+ rebuild!
97
133
  end
98
134
 
99
135
  # Add custom keys to an entry.
@@ -119,29 +155,37 @@ class Rage::Logger
119
155
 
120
156
  # Add a custom tag to an entry.
121
157
  #
122
- # @param tag [String] the tag to add to an entry
158
+ # @param tags [String] the tag to add to an entry
123
159
  # @example
124
160
  # Rage.logger.tagged("ApiCall") do
125
161
  # Rage.logger.info "success"
126
162
  # end
127
- def tagged(tag)
128
- (Thread.current[:rage_logger] ||= { tags: [], context: {} })[:tags] << tag
163
+ def tagged(*tags)
164
+ old_tags = (Thread.current[:rage_logger] ||= { tags: [], context: {} })[:tags]
165
+ Thread.current[:rage_logger][:tags] = old_tags + tags
129
166
  yield(self)
130
167
  ensure
131
- Thread.current[:rage_logger][:tags].pop
168
+ Thread.current[:rage_logger][:tags] = old_tags
132
169
  end
133
170
 
134
171
  alias_method :with_tag, :tagged
135
172
 
173
+ # Check if the debug level is enabled.
136
174
  def debug? = @level <= Logger::DEBUG
175
+ # Check if the error level is enabled.
137
176
  def error? = @level <= Logger::ERROR
177
+ # Check if the fatal level is enabled.
138
178
  def fatal? = @level <= Logger::FATAL
179
+ # Check if the info level is enabled.
139
180
  def info? = @level <= Logger::INFO
181
+ # Check if the warn level is enabled.
140
182
  def warn? = @level <= Logger::WARN
183
+ # Check if the unknown level is enabled.
184
+ def unknown? = @level <= Logger::UNKNOWN
141
185
 
142
186
  private
143
187
 
144
- def define_log_methods
188
+ def rebuild!
145
189
  methods = METHODS_MAP.map do |level_name, level_val|
146
190
  if @logdev.nil? || level_val < @level
147
191
  # logging is disabled or the log level is higher than the current one
@@ -150,25 +194,55 @@ class Rage::Logger
150
194
  false
151
195
  end
152
196
  RUBY
153
- elsif @formatter.class.name.start_with?("Rage::")
154
- # the call was made from within the application and a built-in formatter is used;
155
- # in such case we use the `gen_timestamp` method which is much faster than `Time.now.strftime`;
156
- # it's not a standard approach however, so it's used with built-in formatters only
157
- <<-RUBY
197
+ elsif @external_logger.is_a?(External::Static)
198
+ # an object that implements Ruby's Logger interface is used as a logger
199
+ <<~RUBY
158
200
  def #{level_name}(msg = nil)
159
- @logdev.write(
160
- @formatter.call("#{level_name}".freeze, Iodine::Rack::Utils.gen_timestamp, nil, msg || yield)
161
- )
201
+ #{with_dynamic_tags_and_context do
202
+ <<~RUBY
203
+ @external_logger.wrapped.#{level_name}(
204
+ #{build_formatter_call(level_name, level_val)}
205
+ )
206
+ RUBY
207
+ end}
208
+ end
209
+ RUBY
210
+ elsif @external_logger.is_a?(External::Dynamic)
211
+ # a callable object is used as a logger
212
+ call_method = if @external_logger.wrapped.is_a?(Proc)
213
+ @external_logger.wrapped
214
+ else
215
+ @external_logger.wrapped.method(:call)
216
+ end
217
+
218
+ parameters = Rage::Internal.build_arguments(call_method, {
219
+ severity: ":#{level_name}",
220
+ tags: "logger[:tags].freeze",
221
+ context: "logger[:context].freeze",
222
+ message: "block_given? ? yield : msg",
223
+ request_info: "logger[:final].freeze"
224
+ })
225
+
226
+ <<~RUBY
227
+ def #{level_name}(msg = nil)
228
+ #{with_dynamic_tags_and_context do
229
+ <<~RUBY
230
+ logger = Thread.current[:rage_logger] || { tags: [], context: {} }
231
+ @external_logger.wrapped.call(#{parameters})
232
+ RUBY
233
+ end}
162
234
  end
163
235
  RUBY
164
236
  else
165
- # the call was made from within the application and a custom formatter is used;
166
- # stick to the standard approach of using one of the Log Level constants as sevetiry and `Time.now` as time
167
- <<-RUBY
237
+ <<~RUBY
168
238
  def #{level_name}(msg = nil)
169
- @logdev.write(
170
- @formatter.call(#{level_val}, Time.now, nil, msg || yield)
171
- )
239
+ #{with_dynamic_tags_and_context do
240
+ <<~RUBY
241
+ @logdev.write(
242
+ #{build_formatter_call(level_name, level_val)}
243
+ )
244
+ RUBY
245
+ end}
172
246
  end
173
247
  RUBY
174
248
  end
@@ -176,4 +250,36 @@ class Rage::Logger
176
250
 
177
251
  self.class.class_eval(methods.join("\n"))
178
252
  end
253
+
254
+ def build_formatter_call(level_name, level_val)
255
+ if @formatter.class.name.start_with?("Rage::")
256
+ # a built-in formatter is used - use the `gen_timestamp` method which is much faster than `Time.now.strftime`;
257
+ # it's not a standard approach however, so it's used with built-in formatters only
258
+ <<~RUBY
259
+ @formatter.call("#{level_name}".freeze, Iodine::Rack::Utils.gen_timestamp, nil, block_given? ? yield : msg)
260
+ RUBY
261
+ else
262
+ # a custom formatter is used - stick to the standard approach of using one of the
263
+ # Log Level constants as severity and `Time.now` as time
264
+ <<~RUBY
265
+ @formatter.call(#{level_val}, Time.now, nil, block_given? ? yield : msg)
266
+ RUBY
267
+ end
268
+ end
269
+
270
+ def with_dynamic_tags_and_context
271
+ do_calls, end_calls = [], []
272
+
273
+ if @dynamic_tags
274
+ do_calls << "tagged(*@dynamic_tags.call) do"
275
+ end_calls << "end"
276
+ end
277
+
278
+ if @dynamic_context
279
+ do_calls << "with_context(@dynamic_context.call) do"
280
+ end_calls << "end"
281
+ end
282
+
283
+ "#{do_calls.join("\n")}\n#{yield}\n#{end_calls.join("\n")}"
284
+ end
179
285
  end
@@ -1,3 +1,20 @@
1
+ ##
2
+ # Text formatter for Rage logger.
3
+ #
4
+ # Example log line:
5
+ #
6
+ # ```
7
+ # [fecbba0735355738] timestamp=2025-10-19T11:12:56+00:00 pid=1825 level=info method=GET path=/api/v1/resource controller=Api::V1::ResourceController action=index status=200 duration=0.15
8
+ # ```
9
+ #
10
+ # Use {Rage.configure Rage.configure} to set the formatter:
11
+ #
12
+ # ```ruby
13
+ # Rage.configure do |config|
14
+ # config.log_formatter = Rage::TextFormatter.new
15
+ # end
16
+ # ```
17
+ #
1
18
  class Rage::TextFormatter
2
19
  def initialize
3
20
  @pid = Process.pid
@@ -17,11 +34,13 @@ class Rage::TextFormatter
17
34
 
18
35
  if (final = logger[:final])
19
36
  params, env = final[:params], final[:env]
37
+ tags = tags.map { |tag| "[#{tag}]" }.join
38
+
20
39
  if params && params[:controller]
21
- return "[#{tags[0]}] timestamp=#{timestamp} pid=#{@pid} level=info method=#{env["REQUEST_METHOD"]} path=#{env["PATH_INFO"]} controller=#{Rage::Router::Util.path_to_name(params[:controller])} action=#{params[:action]} #{context_msg}status=#{final[:response][0]} duration=#{final[:duration]}\n"
40
+ return "#{tags} timestamp=#{timestamp} pid=#{@pid} level=info method=#{env["REQUEST_METHOD"]} path=#{env["PATH_INFO"]} controller=#{Rage::Router::Util.path_to_name(params[:controller])} action=#{params[:action]} #{context_msg}status=#{final[:response][0]} duration=#{final[:duration]}\n"
22
41
  else
23
42
  # no controller/action keys are written if there are no params
24
- return "[#{tags[0]}] timestamp=#{timestamp} pid=#{@pid} level=info method=#{env["REQUEST_METHOD"]} path=#{env["PATH_INFO"]} #{context_msg}status=#{final[:response][0]} duration=#{final[:duration]}\n"
43
+ return "#{tags} timestamp=#{timestamp} pid=#{@pid} level=info method=#{env["REQUEST_METHOD"]} path=#{env["PATH_INFO"]} #{context_msg}status=#{final[:response][0]} duration=#{final[:duration]}\n"
25
44
  end
26
45
  end
27
46
 
@@ -17,6 +17,14 @@ class Rage::FiberWrapper
17
17
  def call(env)
18
18
  fiber = Fiber.schedule do
19
19
  @app.call(env)
20
+ rescue Exception => e
21
+ exception_str = "#{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}"
22
+ Rage.logger << exception_str
23
+ if Rage.env.development?
24
+ [500, {}, [exception_str]]
25
+ else
26
+ [500, {}, []]
27
+ end
20
28
  ensure
21
29
  # notify Iodine the request can now be resumed
22
30
  Iodine.publish(Fiber.current.__get_id, "", Iodine::PubSub::PROCESS)
@@ -5,29 +5,24 @@ class Rage::Reloader
5
5
  Iodine.on_state(:on_start) do
6
6
  Rage.code_loader.check_updated!
7
7
  end
8
+
8
9
  @app = app
10
+ @mutex = Mutex.new
9
11
  end
10
12
 
11
13
  def call(env)
12
14
  with_reload do
13
15
  @app.call(env)
14
16
  end
15
- rescue Exception => e
16
- exception_str = "#{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}"
17
- puts(exception_str)
18
- [500, {}, [exception_str]]
19
17
  end
20
18
 
21
19
  private
22
20
 
23
21
  def with_reload
24
- if Rage.code_loader.check_updated!
25
- Fiber.new(blocking: true) {
26
- Rage.code_loader.reload
27
- yield
28
- }.resume
29
- else
30
- yield
22
+ @mutex.synchronize do
23
+ Rage.code_loader.reload if Rage.code_loader.check_updated!
31
24
  end
25
+
26
+ yield
32
27
  end
33
28
  end
data/lib/rage/request.rb CHANGED
@@ -36,8 +36,11 @@ class Rage::Request
36
36
  KNOWN_HTTP_METHODS = (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789).to_set
37
37
 
38
38
  # @private
39
- def initialize(env)
39
+ # @param env [Hash] Rack env
40
+ # @param controller [RageController::API]
41
+ def initialize(env, controller: nil)
40
42
  @env = env
43
+ @controller = controller
41
44
  end
42
45
 
43
46
  # Check if the request was made using TLS/SSL which is if http or https protocol is used inside the URL.
@@ -203,6 +206,20 @@ class Rage::Request
203
206
 
204
207
  alias_method :uuid, :request_id
205
208
 
209
+ # Get the route URI pattern matched for this request.
210
+ # @return [String] the route URI pattern
211
+ # @example
212
+ # # For a route defined as:
213
+ # # get "/users/:id", to: "users#show"
214
+ # request.route_uri_pattern # => "/users/:id"
215
+ def route_uri_pattern
216
+ if @controller
217
+ Rage::Router::Util.route_uri_pattern(@controller.class, @controller.action_name)
218
+ else
219
+ path
220
+ end
221
+ end
222
+
206
223
  private
207
224
 
208
225
  def rack_request
data/lib/rage/response.rb CHANGED
@@ -16,7 +16,7 @@ class Rage::Response
16
16
  # Returns the content of the response as a string. This contains the contents of any calls to `render`.
17
17
  # @return [String]
18
18
  def body
19
- @body[0]
19
+ @body[0] || ""
20
20
  end
21
21
 
22
22
  # Returns the headers for the response.
@@ -31,6 +31,14 @@ class Rage::Router::Util
31
31
  @@names_map[str] = path_to_class(str).name
32
32
  end
33
33
  end
34
+
35
+ @@uri_patterns_map = Hash.new { |h, k| h[k] = {} }
36
+
37
+ def route_uri_pattern(controller_class, action_name)
38
+ @@uri_patterns_map[controller_class][action_name] ||= Rage.__router.routes.find { |route|
39
+ route[:meta][:controller_class] == controller_class && route[:meta][:action] == action_name
40
+ }[:path]
41
+ end
34
42
  end
35
43
 
36
44
  # @private
data/lib/rage/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rage
4
- VERSION = "1.18.0"
4
+ VERSION = "1.19.0"
5
5
  end
data/lib/rage-rb.rb CHANGED
@@ -6,71 +6,115 @@ require "iodine"
6
6
  require "pathname"
7
7
 
8
8
  module Rage
9
+ # Builds the Rage application with the configured middlewares.
9
10
  def self.application
10
11
  with_middlewares(Application.new(__router), config.middleware.middlewares)
11
12
  end
12
13
 
14
+ # Builds the Rage application which delegates Rails requests to `Rails.application`.
13
15
  def self.multi_application
14
16
  Rage::Router::Util::Cascade.new(application, Rails.application)
15
17
  end
16
18
 
19
+ # Shorthand to access {Rage::Cable Rage::Cable}.
20
+ # @return [Rage::Cable]
17
21
  def self.cable
18
22
  Rage::Cable
19
23
  end
20
24
 
25
+ # Shorthand to access {Rage::OpenAPI Rage::OpenAPI}.
26
+ # @return [Rage::OpenAPI]
21
27
  def self.openapi
22
28
  Rage::OpenAPI
23
29
  end
24
30
 
31
+ # Shorthand to access {Rage::Deferred Rage::Deferred}.
32
+ # @return [Rage::Deferred]
25
33
  def self.deferred
26
34
  Rage::Deferred
27
35
  end
28
36
 
37
+ # Shorthand to access {Rage::Events Rage::Events}.
38
+ # @return [Rage::Events]
29
39
  def self.events
30
40
  Rage::Events
31
41
  end
32
42
 
43
+ # Configure routes for the Rage application.
44
+ # @return [Rage::Router::DSL::Handler]
45
+ # @example
46
+ # Rage.routes.draw do
47
+ # root to: "users#index"
48
+ # end
33
49
  def self.routes
34
50
  Rage::Router::DSL.new(__router)
35
51
  end
36
52
 
53
+ # @private
37
54
  def self.__router
38
55
  @__router ||= Rage::Router::Backend.new
39
56
  end
40
57
 
58
+ # @private
59
+ def self.__log_processor
60
+ @__log_processor ||= Rage::LogProcessor.new
61
+ end
62
+
63
+ # Access the Rage configuration.
64
+ # @return [Rage::Configuration] the Rage configuration instance.
41
65
  def self.config
42
66
  @config ||= Rage::Configuration.new
43
67
  end
44
68
 
69
+ # Configure Rage using a block.
70
+ # @example
71
+ # Rage.configure do |config|
72
+ # config.log_level = :debug
73
+ # end
45
74
  def self.configure(&)
46
75
  config.instance_eval(&)
47
76
  config.__finalize
48
77
  end
49
78
 
79
+ # Access the current Rage environment.
80
+ # @return [Rage::Env] the Rage environment instance
81
+ # @example
82
+ # if Rage.env.development?
83
+ # puts "Running in development mode"
84
+ # end
50
85
  def self.env
51
86
  @__env ||= Rage::Env.new(ENV["RAGE_ENV"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development")
52
87
  end
53
88
 
89
+ # Access the current Gem groups based on the Rage environment.
54
90
  def self.groups
55
91
  [:default, Rage.env.to_sym]
56
92
  end
57
93
 
94
+ # Access the root path of the Rage application.
95
+ # @return [Pathname] the root path
58
96
  def self.root
59
97
  @root ||= Pathname.new(".").expand_path
60
98
  end
61
99
 
100
+ # Access the Rage logger.
101
+ # @return [Rage::Logger] the Rage logger instance
62
102
  def self.logger
63
103
  @logger ||= config.logger
64
104
  end
65
105
 
106
+ # Load middlewares into the Rage application.
107
+ # @deprecated This method is deprecated and has been merged into `Rage.application`.
66
108
  def self.load_middlewares(_)
67
109
  puts "`Rage.load_middlewares` is deprecated and has been merged into `Rage.application`. Please remove this call."
68
110
  end
69
111
 
112
+ # @private
70
113
  def self.code_loader
71
114
  @code_loader ||= Rage::CodeLoader.new
72
115
  end
73
116
 
117
+ # @private
74
118
  def self.patch_active_record_connection_pool
75
119
  patch = proc do
76
120
  is_connected = ActiveRecord::Base.connection_pool rescue false
@@ -94,6 +138,7 @@ module Rage
94
138
  end
95
139
  end
96
140
 
141
+ # Load Rake tasks for the Rage application.
97
142
  def self.load_tasks
98
143
  Rage::Tasks.init
99
144
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rage-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.18.0
4
+ version: 1.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roman Samoilov
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-10-29 00:00:00.000000000 Z
10
+ date: 2025-12-03 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: thor
@@ -144,8 +144,8 @@ files:
144
144
  - lib/rage/cookies.rb
145
145
  - lib/rage/deferred/backends/disk.rb
146
146
  - lib/rage/deferred/backends/nil.rb
147
+ - lib/rage/deferred/context.rb
147
148
  - lib/rage/deferred/deferred.rb
148
- - lib/rage/deferred/metadata.rb
149
149
  - lib/rage/deferred/proxy.rb
150
150
  - lib/rage/deferred/queue.rb
151
151
  - lib/rage/deferred/task.rb
@@ -159,6 +159,7 @@ files:
159
159
  - lib/rage/fiber_scheduler.rb
160
160
  - lib/rage/hooks.rb
161
161
  - lib/rage/internal.rb
162
+ - lib/rage/log_processor.rb
162
163
  - lib/rage/logger/json_formatter.rb
163
164
  - lib/rage/logger/logger.rb
164
165
  - lib/rage/logger/text_formatter.rb
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- ##
4
- # Metadata for deferred tasks.
5
- # The class encapsulates the metadata associated with a deferred task, and allows to store it without modifying the task instance.
6
- #
7
- class Rage::Deferred::Metadata
8
- def self.build(task, args, kwargs)
9
- request_id = Thread.current[:rage_logger][:tags][0] if Thread.current[:rage_logger]
10
-
11
- [
12
- task,
13
- args.empty? ? nil : args,
14
- kwargs.empty? ? nil : kwargs,
15
- nil,
16
- request_id
17
- ]
18
- end
19
-
20
- def self.get_task(metadata)
21
- metadata[0]
22
- end
23
-
24
- def self.get_args(metadata)
25
- metadata[1]
26
- end
27
-
28
- def self.get_kwargs(metadata)
29
- metadata[2]
30
- end
31
-
32
- def self.get_attempts(metadata)
33
- metadata[3]
34
- end
35
-
36
- def self.get_request_id(metadata)
37
- metadata[4]
38
- end
39
-
40
- def self.inc_attempts(metadata)
41
- metadata[3] = metadata[3].to_i + 1
42
- end
43
- end