Skip to content

Feature: Add optional output to maintenance task to display output #1251

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

scotttam
Copy link

@scotttam scotttam commented Jul 18, 2025

Add Optional Output Logging for Maintenance Tasks

Summary

This PR introduces an optional output logging feature for maintenance tasks, allowing
tasks to capture and display detailed execution logs in the web UI. The output is
stored persistently in the database and displayed in real-time during task execution.

Why This Change?

Maintenance tasks often perform complex operations on large datasets. Previously,
there was no built-in way to track what the task was doing beyond basic progress
metrics. This feature enables:

  • Better visibility into task execution details
  • Debugging capabilities for failed or unexpected behaviors
  • Audit trails for compliance and verification
  • Progress tracking with meaningful context

Implementation Details

Database Changes

  • Added output text column to maintenance_tasks_runs table via migration

Core Functionality

  • Added log_output(message) method to MaintenanceTasks::Task base class
  • Added append_output(new_output) method to MaintenanceTasks::Run model
  • Output is accumulated throughout task execution and persists in the database
  • Bidirectional reference between Run and Task instances enables output logging

UI Changes

  • Added new _output.html.erb partial to display task output
  • Output is shown in a formatted box on the run details page
  • Only displayed when output column exists and contains data

Usage Example

module Maintenance
  class DataCleanupTask < MaintenanceTasks::Task
    def collection
      User.where(last_sign_in_at: nil)
    end

    def process(user)
      log_output("Processing user: #{user.email} (ID: #{user.id})")

      if user.created_at < 1.year.ago
        log_output("  -> Marking user for cleanup")
        user.update!(cleanup_flag: true)
        log_output("  -> Successfully processed")
      else
        log_output("  -> Skipping: user created recently")
      end
    end
  end
end

Visual Example

Screenshot 2025-07-17 at 7 05 03 PM

Testing

  • Added comprehensive test coverage in task_output_test.rb
  • Tests cover collection-based tasks, no-collection tasks, output accumulation, and
    edge cases
  • Added example OutputTestTask in test dummy app

Documentation

  • Updated README with detailed usage instructions and examples
  • Includes migration instructions and feature description

Backward Compatibility

  • Feature is completely optional - existing tasks continue to work unchanged
  • log_output gracefully handles missing output column
  • UI conditionally renders output section only when column exists

Next Steps

Users need to run the migration to enable this feature:
rails generate migration AddOutputToMaintenanceTasksRuns output:text
rails db:migrate

@scotttam scotttam force-pushed the add_output_column branch 3 times, most recently from 09139ec to 5bb6f0c Compare July 18, 2025 01:55
Copy link
Contributor

@adrianna-chang-shopify adrianna-chang-shopify left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just use Rails.logger in your task? Do we need to persist log data to the run itself? This seems like it would get out of hand pretty fast on tasks that execute on thousands of records...

Copy link
Contributor

@nvasilevski nvasilevski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to agree with Adrianna, in our experience this solution will not sustain in tasks that iterate over millions of records. And looking at the examples, from my personal experience I wouldn't even recommend issuing logs such as Marking user for cleanup or Successfully processed, those things should be implied by the "green path" of the task, meaning that if no error happened and the iteration counter went up it should be safe to assume that the task did what it was supposed to do. Otherwise a log filled with entries like "business as usual" becomes extremely hard to utilize for debugging purposes or just gets truncated depending on the infra setup

return unless self.class.column_names.include?("output")

# Reload output from database to get the latest value
current_output = self.class.where(id: id).pluck(:output).first || ""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately I don't think we can afford adding a feature like that to the framework. It may work okay for smaller applications but for large applications with a lot of rows to iterate over the approach of transferring large chunks for data back-and-forth doesn't scale well

@scotttam
Copy link
Author

Thanks everyone for the feedback!

Rails.logger isn't a great IMO as it requires running the task and then watching the logs, which in production is kinda of a pain and the format is terrible. Ideally I'd like to see the output immediately from the run.

I understand your concern about larger applications with multiple rows touching the database. Another idea I had was rather than writing to the database was writing the output to Rails.cache. That way, the output could be short lived (TTL 1 hour?), and we wouldn't have to have a migration or touch the database. How would folks feel about that?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants