Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,46 @@ end

We recommend adding guards in your route to ensure that the proper authentication is in place to ensure that users are authenticated so that if this were ever to be rendered in production, you would be covered. The best way for that is [router constraints](https://guides.rubyonrails.org/routing.html#specifying-constraints)

## Debugging

RakeUi emits public-safe structured Rails debug logs for rake task execution lifecycle events. Enable them with the host Rails app's normal debug log configuration, for example:

```bash
RAILS_LOG_LEVEL=debug
```

Every RakeUi debug event uses the same set of keys:

| Field | Meaning |
| --- | --- |
| `component` | Always `rake-ui`. |
| `event` | Stable lifecycle event name. |
| `rails_app` | Host Rails application's module name. |
| `task_name` | Rake task name. |
| `task_log_id` | RakeUi task execution log id, or `nil` before one exists. |

RakeUi intentionally does not log command strings, argument values, environment values, authentication data, or application-specific metadata.

| Event | Emitted when | Meaning |
| --- | --- | --- |
| `rake_ui.task_execution.requested` | `RakeUi::RakeTask#call` begins. | The host app accepted a request to execute a rake task. `task_log_id` is not available yet. |
| `rake_ui.task_log.created` | `RakeUi::RakeTaskLog.build_new_for_command` creates the log file. | A durable local execution log file now exists. |
| `rake_ui.task_execution.forked` | The execution process is forked. | Async task execution has been handed to a child process. |
| `rake_ui.task_execution.finished_marker_written` | The child process writes the finished marker after command execution. | The rake-ui local log should now indicate completion. |
| `rake_ui.task_execution.failed` | An observable exception occurs during setup/forking. | Execution setup failed before normal async handoff completed. |

Example:

```json
{
"component": "rake-ui",
"event": "rake_ui.task_execution.forked",
"rails_app": "MyApp",
"task_name": "db:migrate",
"task_log_id": "2026-05-11-10-22-33-0400____db%3Amigrate"
}
```

## Testing

`bundle exec rake test`
Expand Down
17 changes: 12 additions & 5 deletions app/models/rake_ui/rake_task.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ def internal_task?
end

def call(args: nil, environment: nil)
RakeUi::DebugLogger.debug("rake_ui.task_execution.requested", task_name: name)
rake_command = build_rake_command(args: args, environment: environment)

rake_task_log = RakeUi::RakeTaskLog.build_new_for_command(
Expand All @@ -92,15 +93,21 @@ def call(args: nil, environment: nil)
raker_id: id
)

puts "[rake_ui] [rake_task] [forked] #{rake_task_log.rake_command_with_logging}"
fork_task_execution(rake_task_log)

fork do
system(rake_task_log.rake_command_with_logging)
RakeUi::DebugLogger.debug("rake_ui.task_execution.forked", task_name: name, task_log_id: rake_task_log.id)
rake_task_log
rescue
RakeUi::DebugLogger.debug("rake_ui.task_execution.failed", task_name: name, task_log_id: rake_task_log&.id)
raise
end

def fork_task_execution(rake_task_log)
Process.fork do
system(rake_task_log.rake_command_with_logging)
system(rake_task_log.command_to_mark_log_finished)
RakeUi::DebugLogger.debug("rake_ui.task_execution.finished_marker_written", task_name: name, task_log_id: rake_task_log.id)
end

rake_task_log
end

# returns an invokable rake command
Expand Down
2 changes: 2 additions & 0 deletions app/models/rake_ui/rake_task_log.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ def self.build_new_for_command(name:, rake_definition_file:, rake_command:, rake
f.puts TASK_HEADER_OUTPUT_DELIMITER.to_s
end

RakeUi::DebugLogger.debug("rake_ui.task_log.created", task_name: name, task_log_id: id)

new(id: id,
name: name,
args: args,
Expand Down
1 change: 1 addition & 0 deletions lib/rake-ui.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require "rake-ui/engine"
require "rake-ui/debug_logger"

module RakeUi
mattr_accessor :allow_production
Expand Down
28 changes: 28 additions & 0 deletions lib/rake-ui/debug_logger.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

module RakeUi
module DebugLogger
module_function

def debug(event, task_name: nil, task_log_id: nil)
return unless defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger

Rails.logger.debug(
component: "rake-ui",
event: event,
rails_app: rails_app_name,
task_name: task_name,
task_log_id: task_log_id
)
end

def rails_app_name
application_class = Rails.application.class
if application_class.respond_to?(:module_parent_name)
application_class.module_parent_name
else
application_class.name.to_s.split("::").first
end
end
end
end
2 changes: 1 addition & 1 deletion test/integration/rake_tasks_request_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class RakeTasksRequestTest < ActionDispatch::IntegrationTest
def mock_task.id
"some_identifier"
end
mock_task.expect(:call, OpenStruct.new(id: "1234_path"), {args: "1,2,3", environment: "FOO=bar"})
mock_task.expect(:call, OpenStruct.new(id: "1234_path"), [], args: "1,2,3", environment: "FOO=bar")

mock_find_by_id = lambda do |args|
assert args, "some_identifier"
Expand Down
58 changes: 58 additions & 0 deletions test/rake_ui/debug_logger_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# frozen_string_literal: true

require "test_helper"

class DebugLoggerTest < ActiveSupport::TestCase
test "emits public safe structured debug logs with a stable shape" do
messages = []
logger = Class.new do
def initialize(messages)
@messages = messages
end

def debug(message)
@messages << message
end
end.new(messages)

Rails.stub(:logger, logger) do
RakeUi::DebugLogger.debug(
"rake_ui.task_execution.requested",
task_name: "db:migrate",
task_log_id: "2026-05-11-10-22-33-0400____db%3Amigrate"
)
end

assert_equal 1, messages.length
assert_equal(
{
component: "rake-ui",
event: "rake_ui.task_execution.requested",
rails_app: "Dummy",
task_name: "db:migrate",
task_log_id: "2026-05-11-10-22-33-0400____db%3Amigrate"
},
messages.first
)
end

test "keeps the same keys when task log id is not available yet" do
messages = []
logger = Class.new do
def initialize(messages)
@messages = messages
end

def debug(message)
@messages << message
end
end.new(messages)

Rails.stub(:logger, logger) do
RakeUi::DebugLogger.debug("rake_ui.task_execution.requested", task_name: "db:migrate")
end

assert_equal [:component, :event, :rails_app, :task_name, :task_log_id], messages.first.keys
assert_nil messages.first[:task_log_id]
end
end
25 changes: 25 additions & 0 deletions test/rake_ui/rake_task_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,28 @@ def get_double_nested_task
RakeUi::RakeTask.find_by_id(id)
end
end

class RakeTaskDebugLoggingTest < ActiveSupport::TestCase
test "call emits public safe lifecycle debug events" do
task = RakeUi::RakeTask.find_by_id(RakeUi::RakeTask.to_safe_identifier("regular"))
events = []
debug = lambda do |event, task_name:, task_log_id: nil|
events << {event: event, task_name: task_name, task_log_id: task_log_id}
end

RakeUi::DebugLogger.stub(:debug, debug) do
task.stub(:fork_task_execution, 1234) do
task.call
end
end

assert_equal [
"rake_ui.task_execution.requested",
"rake_ui.task_log.created",
"rake_ui.task_execution.forked"
], events.map { |event| event[:event] }
assert events.all? { |event| event[:task_name] == "regular" }
assert_nil events.first[:task_log_id]
assert events[1..].all? { |event| event[:task_log_id].present? }
end
end
Loading