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.
@@ -9,7 +9,7 @@ class RageController::API
9
9
  # returns the name of the newly defined method;
10
10
  # rubocop:disable Layout/IndentationWidth, Layout/EndAlignment, Layout/HeredocIndentation
11
11
  def __register_action(action)
12
- raise Rage::Errors::RouterError, "The action '#{action}' could not be found for #{self}" unless method_defined?(action)
12
+ raise Rage::Errors::RouterError, "The action `#{action}` could not be found in the `#{self}` controller. This is likely due to route helpers pointing to non-existent actions in the controller. Please check your routes and ensure that all referenced actions exist." unless method_defined?(action)
13
13
 
14
14
  around_actions_total = 0
15
15
 
@@ -153,7 +153,13 @@ class RageController::API
153
153
  <<~RUBY
154
154
  context = {}
155
155
  append_info_to_payload(context)
156
- Thread.current[:rage_logger][:context] = context
156
+
157
+ log_context = Thread.current[:rage_logger][:context]
158
+ if log_context.empty?
159
+ Thread.current[:rage_logger][:context] = context
160
+ else
161
+ Thread.current[:rage_logger][:context] = log_context.merge(context)
162
+ end
157
163
  RUBY
158
164
  end}
159
165
  end
@@ -448,7 +454,7 @@ class RageController::API
448
454
  # Get the request object. See {Rage::Request}.
449
455
  # @return [Rage::Request]
450
456
  def request
451
- @request ||= Rage::Request.new(@__env)
457
+ @request ||= Rage::Request.new(@__env, controller: self)
452
458
  end
453
459
 
454
460
  # Get the response object. See {Rage::Response}.
@@ -635,6 +641,12 @@ class RageController::API
635
641
  !still_fresh
636
642
  end
637
643
 
644
+ # Get the name of the currently executed action.
645
+ # @return [String] the name of the currently executed action
646
+ def action_name
647
+ @__params[:action]
648
+ end
649
+
638
650
  # @private
639
651
  # for comatibility with `Rails.application.routes.recognize_path`
640
652
  def self.binary_params_for?(_)
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # Context for deferred tasks.
5
+ # The class encapsulates the context associated with a deferred task, and allows to store it without modifying the task instance.
6
+ #
7
+ class Rage::Deferred::Context
8
+ def self.build(task, args, kwargs, storage: nil)
9
+ logger = Thread.current[:rage_logger]
10
+
11
+ [
12
+ task,
13
+ args.empty? ? nil : args,
14
+ kwargs.empty? ? nil : kwargs,
15
+ nil,
16
+ logger&.dig(:tags),
17
+ logger&.dig(:context)
18
+ ]
19
+ end
20
+
21
+ def self.get_task(context)
22
+ context[0]
23
+ end
24
+
25
+ def self.get_args(context)
26
+ context[1]
27
+ end
28
+
29
+ def self.get_kwargs(context)
30
+ context[2]
31
+ end
32
+
33
+ def self.get_attempts(context)
34
+ context[3]
35
+ end
36
+
37
+ def self.inc_attempts(context)
38
+ context[3] = context[3].to_i + 1
39
+ end
40
+
41
+ def self.get_log_tags(context)
42
+ context[4]
43
+ end
44
+
45
+ def self.get_log_context(context)
46
+ context[5]
47
+ end
48
+ end
@@ -88,7 +88,7 @@ end
88
88
  require_relative "task"
89
89
  require_relative "queue"
90
90
  require_relative "proxy"
91
- require_relative "metadata"
91
+ require_relative "context"
92
92
  require_relative "backends/disk"
93
93
  require_relative "backends/nil"
94
94
 
@@ -10,7 +10,7 @@ class Rage::Deferred::Queue
10
10
  end
11
11
 
12
12
  # Write the task to the storage and schedule it for execution.
13
- def enqueue(task_metadata, delay: nil, delay_until: nil, task_id: nil)
13
+ def enqueue(context, delay: nil, delay_until: nil, task_id: nil)
14
14
  apply_backpressure if @backpressure
15
15
 
16
16
  publish_in, publish_at = if delay
@@ -21,14 +21,14 @@ class Rage::Deferred::Queue
21
21
  [delay_until_i - current_time_i, delay_until_i] if delay_until_i > current_time_i
22
22
  end
23
23
 
24
- persisted_task_id = @backend.add(task_metadata, publish_at:, task_id:)
25
- schedule(persisted_task_id, task_metadata, publish_in:)
24
+ persisted_task_id = @backend.add(context, publish_at:, task_id:)
25
+ schedule(persisted_task_id, context, publish_in:)
26
26
  end
27
27
 
28
28
  # Schedule the task for execution.
29
- def schedule(task_id, task_metadata, publish_in: nil)
29
+ def schedule(task_id, context, publish_in: nil)
30
30
  publish_in_ms = publish_in.to_i * 1_000 if publish_in && publish_in > 0
31
- task = Rage::Deferred::Metadata.get_task(task_metadata)
31
+ task = Rage::Deferred::Context.get_task(context)
32
32
  @backlog_size += 1 unless publish_in_ms
33
33
 
34
34
  Iodine.run_after(publish_in_ms) do
@@ -38,14 +38,14 @@ class Rage::Deferred::Queue
38
38
  Fiber.schedule do
39
39
  Iodine.task_inc!
40
40
 
41
- is_completed = task.new.__perform(task_metadata)
41
+ is_completed = task.new.__perform(context)
42
42
 
43
43
  if is_completed
44
44
  @backend.remove(task_id)
45
45
  else
46
- attempts = Rage::Deferred::Metadata.inc_attempts(task_metadata)
46
+ attempts = Rage::Deferred::Context.inc_attempts(context)
47
47
  if task.__should_retry?(attempts)
48
- enqueue(task_metadata, delay: task.__next_retry_in(attempts), task_id:)
48
+ enqueue(context, delay: task.__next_retry_in(attempts), task_id:)
49
49
  else
50
50
  @backend.remove(task_id)
51
51
  end
@@ -41,34 +41,36 @@ module Rage::Deferred::Task
41
41
  end
42
42
 
43
43
  # @private
44
- def __with_optional_log_tag(tag)
45
- if tag
46
- Rage.logger.tagged(tag) { yield }
47
- else
48
- yield
44
+ def __perform(context)
45
+ args = Rage::Deferred::Context.get_args(context)
46
+ kwargs = Rage::Deferred::Context.get_kwargs(context)
47
+ attempts = Rage::Deferred::Context.get_attempts(context)
48
+
49
+ restore_log_info(context)
50
+
51
+ task_log_context = { task: self.class.name }
52
+ task_log_context[:attempt] = attempts + 1 if attempts
53
+
54
+ Rage.logger.with_context(task_log_context) do
55
+ perform(*args, **kwargs)
56
+ true
57
+ rescue Rage::Deferred::TaskFailed
58
+ false
59
+ rescue Exception => e
60
+ Rage.logger.error("Deferred task failed with exception: #{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}")
61
+ false
49
62
  end
50
63
  end
51
64
 
52
- # @private
53
- def __perform(metadata)
54
- args = Rage::Deferred::Metadata.get_args(metadata)
55
- kwargs = Rage::Deferred::Metadata.get_kwargs(metadata)
56
- attempts = Rage::Deferred::Metadata.get_attempts(metadata)
57
- request_id = Rage::Deferred::Metadata.get_request_id(metadata)
58
-
59
- context = { task: self.class.name }
60
- context[:attempt] = attempts + 1 if attempts
65
+ private def restore_log_info(context)
66
+ log_tags = Rage::Deferred::Context.get_log_tags(context)
67
+ log_context = Rage::Deferred::Context.get_log_context(context)
61
68
 
62
- Rage.logger.with_context(context) do
63
- __with_optional_log_tag(request_id) do
64
- perform(*args, **kwargs)
65
- true
66
- rescue Rage::Deferred::TaskFailed
67
- false
68
- rescue Exception => e
69
- Rage.logger.error("Deferred task failed with exception: #{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}")
70
- false
71
- end
69
+ if log_tags.is_a?(Array)
70
+ Thread.current[:rage_logger] = { tags: log_tags, context: log_context }
71
+ elsif log_tags
72
+ # support the previous format where only `request_id` was passed
73
+ Thread.current[:rage_logger] = { tags: [log_tags], context: {} }
72
74
  end
73
75
  end
74
76
 
@@ -79,10 +81,12 @@ module Rage::Deferred::Task
79
81
  module ClassMethods
80
82
  def enqueue(*args, delay: nil, delay_until: nil, **kwargs)
81
83
  Rage::Deferred.__queue.enqueue(
82
- Rage::Deferred::Metadata.build(self, args, kwargs),
84
+ Rage::Deferred::Context.build(self, args, kwargs),
83
85
  delay:,
84
86
  delay_until:
85
87
  )
88
+
89
+ nil
86
90
  end
87
91
 
88
92
  # @private
data/lib/rage/env.rb CHANGED
@@ -1,5 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ ##
4
+ # {Rage::Env Rage::Env} represents the current environment of the Rage application.
5
+ #
6
+ # The class automatically detects the application's current environment by checking the `RAGE_ENV`, `RAILS_ENV`, and `RACK_ENV` environment variables.
7
+ # It provides predicate methods to check which environment is active.
8
+ #
9
+ # @example
10
+ # # Start the application with `rage s -e preprod`
11
+ # Rage.env.development? # => false
12
+ # Rage.env.preprod? # => true
13
+ #
3
14
  class Rage::Env
4
15
  STANDARD_ENVS = %w(development test staging production)
5
16
 
@@ -28,6 +39,10 @@ class Rage::Env
28
39
  @env.to_sym
29
40
  end
30
41
 
42
+ def to_str
43
+ @env
44
+ end
45
+
31
46
  def to_s
32
47
  @env
33
48
  end
data/lib/rage/hooks.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # @private
3
4
  module Hooks
4
5
  def hooks
5
6
  @hooks ||= Hash.new { |h, k| h[k] = [] }
data/lib/rage/internal.rb CHANGED
@@ -30,6 +30,20 @@ class Rage::Internal
30
30
  RUBY
31
31
  end
32
32
 
33
+ # Build a string representation of keyword arguments based on the parameters expected by the method.
34
+ # @param method [Method, Proc] the method to build arguments for
35
+ # @param arguments [Hash] the arguments to include in the string representation
36
+ # @return [String] the string representation of the method arguments
37
+ def build_arguments(method, arguments)
38
+ expected_parameters = method.parameters
39
+
40
+ arguments.filter_map { |arg_name, arg_value|
41
+ if expected_parameters.any? { |param_type, param_name| param_name == arg_name || param_type == :keyrest }
42
+ "#{arg_name}: #{arg_value}"
43
+ end
44
+ }.join(", ")
45
+ end
46
+
33
47
  private
34
48
 
35
49
  def dynamic_name_seed
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Rage::LogProcessor
4
+ DEFAULT_LOG_CONTEXT = {}.freeze
5
+ private_constant :DEFAULT_LOG_CONTEXT
6
+
7
+ attr_reader :dynamic_tags, :dynamic_context
8
+
9
+ def initialize
10
+ rebuild!
11
+ end
12
+
13
+ def add_custom_context(context_objects)
14
+ @custom_context = context_objects
15
+ rebuild!
16
+ end
17
+
18
+ def add_custom_tags(tag_objects)
19
+ @custom_tags = tag_objects
20
+ rebuild!
21
+ end
22
+
23
+ def finalize_request_logger(env, response, params)
24
+ logger = Thread.current[:rage_logger]
25
+
26
+ duration = (
27
+ (Process.clock_gettime(Process::CLOCK_MONOTONIC) - logger[:request_start]) * 1000
28
+ ).round(2)
29
+
30
+ logger[:final] = { env:, params:, response:, duration: }
31
+ Rage.logger.info(nil)
32
+ logger[:final] = nil
33
+ end
34
+
35
+ private
36
+
37
+ def build_static_tags
38
+ calls = @custom_tags&.filter_map&.with_index do |tag_object, i|
39
+ if tag_object.is_a?(String)
40
+ "@custom_tags[#{i}]"
41
+ elsif tag_object.respond_to?(:to_str)
42
+ "@custom_tags[#{i}].to_str"
43
+ end
44
+ end
45
+
46
+ unless calls&.any?
47
+ return "[env[\"rage.request_id\"]]"
48
+ end
49
+
50
+ "[env[\"rage.request_id\"], #{calls.join(", ")}]"
51
+ end
52
+
53
+ def build_static_context
54
+ calls = @custom_context&.filter_map&.with_index do |context_object, i|
55
+ "@custom_context[#{i}]" if context_object.is_a?(Hash)
56
+ end
57
+
58
+ unless calls&.any?
59
+ return "DEFAULT_LOG_CONTEXT"
60
+ end
61
+
62
+ if calls.one?
63
+ calls[0]
64
+ else
65
+ "{}.merge!(#{calls.join(", ")})"
66
+ end
67
+ end
68
+
69
+ def build_dynamic_tags_proc
70
+ calls = @custom_tags&.filter_map&.with_index do |tag_object, i|
71
+ if tag_object.respond_to?(:call)
72
+ "*@custom_tags[#{i}].call"
73
+ end
74
+ end
75
+
76
+ return unless calls&.any?
77
+
78
+ eval <<~RUBY
79
+ ->() do
80
+ [#{calls.join(", ")}]
81
+ end
82
+ RUBY
83
+ end
84
+
85
+ def build_dynamic_context_proc
86
+ calls = @custom_context&.filter_map&.with_index do |context_object, i|
87
+ if context_object.respond_to?(:call)
88
+ "@custom_context[#{i}].call || DEFAULT_LOG_CONTEXT"
89
+ end
90
+ end
91
+
92
+ return unless calls&.any?
93
+
94
+ eval <<~RUBY
95
+ ->() do
96
+ {}.merge!(#{calls.join(", ")})
97
+ end
98
+ RUBY
99
+ end
100
+
101
+ def rebuild!
102
+ @dynamic_tags = build_dynamic_tags_proc
103
+ @dynamic_context = build_dynamic_context_proc
104
+
105
+ self.class.class_eval <<~RUBY, __FILE__, __LINE__ + 1
106
+ def init_request_logger(env)
107
+ env["rage.request_id"] ||= Iodine::Rack::Utils.gen_request_tag
108
+
109
+ Thread.current[:rage_logger] = {
110
+ tags: #{build_static_tags},
111
+ context: #{build_static_context},
112
+ request_start: Process.clock_gettime(Process::CLOCK_MONOTONIC)
113
+ }
114
+ end
115
+ RUBY
116
+ end
117
+ end
@@ -1,3 +1,22 @@
1
+ ##
2
+ # JSON formatter for Rage logger.
3
+ #
4
+ # Produces log lines in JSON format, including tags, context, and request details if available.
5
+ #
6
+ # Example log line:
7
+ #
8
+ # ```json
9
+ # {"tags":["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}
10
+ # ```
11
+ #
12
+ # Use {Rage.configure Rage.configure} to set the formatter:
13
+ #
14
+ # ```ruby
15
+ # Rage.configure do |config|
16
+ # config.log_formatter = Rage::JSONFormatter.new
17
+ # end
18
+ # ```
19
+ #
1
20
  class Rage::JSONFormatter
2
21
  def initialize
3
22
  @pid = Process.pid.to_s
@@ -15,32 +34,32 @@ class Rage::JSONFormatter
15
34
  context.each { |k, v| context_msg << "\"#{k}\":#{v.to_json}," }
16
35
  end
17
36
 
18
- if (final = logger[:final])
19
- params, env = final[:params], final[:env]
20
- if params && params[:controller]
21
- return "{\"tags\":[\"#{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"
22
- else
23
- # no controller/action keys are written if there are no params
24
- return "{\"tags\":[\"#{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"
25
- end
26
- end
27
-
28
- if tags.length == 1
29
- tags_msg = "{\"tags\":[\"#{tags[0]}\"],\"timestamp\":\"#{timestamp}\",\"pid\":\"#{@pid}\",\"level\":\"#{severity}\""
37
+ tags_msg = if tags.length == 1
38
+ "{\"tags\":[\"#{tags[0]}\"],"
30
39
  elsif tags.length == 2
31
- tags_msg = "{\"tags\":[\"#{tags[0]}\",\"#{tags[1]}\"],\"timestamp\":\"#{timestamp}\",\"pid\":\"#{@pid}\",\"level\":\"#{severity}\""
40
+ "{\"tags\":[\"#{tags[0]}\",\"#{tags[1]}\"],"
32
41
  elsif tags.length == 0
33
- tags_msg = "{\"timestamp\":\"#{timestamp}\",\"pid\":\"#{@pid}\",\"level\":\"#{severity}\""
42
+ "{"
34
43
  else
35
- tags_msg = "{\"tags\":[\"#{tags[0]}\",\"#{tags[1]}\""
44
+ msg = "{\"tags\":[\"#{tags[0]}\",\"#{tags[1]}\""
36
45
  i = 2
37
46
  while i < tags.length
38
- tags_msg << ",\"#{tags[i]}\""
47
+ msg << ",\"#{tags[i]}\""
39
48
  i += 1
40
49
  end
41
- tags_msg << "],\"timestamp\":\"#{timestamp}\",\"pid\":\"#{@pid}\",\"level\":\"#{severity}\""
50
+ msg << "],"
51
+ end
52
+
53
+ if (final = logger[:final])
54
+ params, env = final[:params], final[:env]
55
+ if params && params[:controller]
56
+ return "#{tags_msg}\"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"
57
+ else
58
+ # no controller/action keys are written if there are no params
59
+ return "#{tags_msg}\"timestamp\":\"#{timestamp}\",\"pid\":\"#{@pid}\",\"level\":\"info\",\"method\":\"#{env["REQUEST_METHOD"]}\",\"path\":\"#{env["PATH_INFO"]}\",#{context_msg}\"status\":#{final[:response][0]},\"duration\":#{final[:duration]}}\n"
60
+ end
42
61
  end
43
62
 
44
- "#{tags_msg},#{context_msg}\"message\":\"#{message}\"}\n"
63
+ "#{tags_msg}\"timestamp\":\"#{timestamp}\",\"pid\":\"#{@pid}\",\"level\":\"#{severity}\",#{context_msg}\"message\":\"#{message}\"}\n"
45
64
  end
46
65
  end