job logs
This commit is contained in:
parent
7022b48bda
commit
34be944b96
16 changed files with 333 additions and 2 deletions
32
app/controllers/job_logs_controller.rb
Normal file
32
app/controllers/job_logs_controller.rb
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
class JobLogsController < ApplicationController
|
||||
before_action :set_job_log, only: [ :show ]
|
||||
|
||||
def index
|
||||
@job_logs = if params[:server_id]
|
||||
server = current_user.servers.find(params[:server_id])
|
||||
server.job_logs.recent
|
||||
else
|
||||
JobLog.where(server_id: current_user.servers.pluck(:id))
|
||||
.or(JobLog.where(server_id: nil))
|
||||
.recent
|
||||
end
|
||||
@job_logs = @job_logs.page(params[:page]).per(20) if defined?(Kaminari)
|
||||
end
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_job_log
|
||||
@job_log = JobLog.find(params[:id])
|
||||
# Authorize: user must own the server or job must be global (no server)
|
||||
unless @job_log.server_id.nil? || current_user.servers.exists?(id: @job_log.server_id)
|
||||
redirect_to job_logs_path, alert: "Not authorized"
|
||||
end
|
||||
end
|
||||
|
||||
def authorize_user!
|
||||
redirect_to root_path, alert: "Please log in" unless current_user
|
||||
end
|
||||
end
|
||||
2
app/helpers/job_logs_helper.rb
Normal file
2
app/helpers/job_logs_helper.rb
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
module JobLogsHelper
|
||||
end
|
||||
|
|
@ -4,4 +4,54 @@ class ApplicationJob < ActiveJob::Base
|
|||
|
||||
# Most jobs are safe to ignore if the underlying records are no longer available
|
||||
# discard_on ActiveJob::DeserializationError
|
||||
|
||||
around_perform do |_job, block|
|
||||
log_entry = create_job_log
|
||||
@job_log = log_entry
|
||||
|
||||
begin
|
||||
log_entry.update!(status: :running, started_at: Time.current)
|
||||
log("Job started: #{self.class.name}")
|
||||
|
||||
block.call
|
||||
|
||||
log("Job completed successfully")
|
||||
log_entry.update!(status: :completed, finished_at: Time.current)
|
||||
rescue => e
|
||||
log("Job failed: #{e.class.name} - #{e.message}")
|
||||
log(e.backtrace.first(5).join("\n"))
|
||||
log_entry.update!(
|
||||
status: :failed,
|
||||
finished_at: Time.current,
|
||||
error_message: "#{e.class.name}: #{e.message}"
|
||||
)
|
||||
raise
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_job_log
|
||||
server_id = extract_server_id_from_arguments
|
||||
|
||||
JobLog.create!(
|
||||
job_class: self.class.name,
|
||||
job_id: job_id,
|
||||
arguments: arguments.to_json,
|
||||
server_id: server_id,
|
||||
status: :pending
|
||||
)
|
||||
end
|
||||
|
||||
def extract_server_id_from_arguments
|
||||
# First argument is typically server_id for server-related jobs
|
||||
return nil if arguments.empty?
|
||||
arg = arguments.first
|
||||
arg.is_a?(Integer) ? arg : nil
|
||||
end
|
||||
|
||||
def log(message)
|
||||
return unless @job_log
|
||||
@job_log.append_log(message)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,19 +3,32 @@ class SpawnServerJob < ApplicationJob
|
|||
|
||||
def perform(server_id)
|
||||
server = Server.find(server_id)
|
||||
log "Found server: #{server.name}"
|
||||
|
||||
begin
|
||||
log "Starting server spawn process..."
|
||||
L4dServer::Launcher.spawn(server)
|
||||
log "Server spawned successfully"
|
||||
|
||||
server.update(status: :starting)
|
||||
log "Server status updated to starting"
|
||||
|
||||
# Generate a random RCON password for health checks
|
||||
server.update(rcon_password: SecureRandom.hex(16))
|
||||
rcon_password = SecureRandom.hex(16)
|
||||
server.update(rcon_password: rcon_password)
|
||||
log "RCON password generated and saved"
|
||||
|
||||
Activity.log(server.user, "spawned_success", "Server", server.id, { name: server.name })
|
||||
log "Activity logged"
|
||||
rescue StandardError => e
|
||||
log "ERROR: Server spawn failed: #{e.message}"
|
||||
log "Backtrace: #{e.backtrace.first(3).join("\n")}"
|
||||
|
||||
Rails.logger.error("Server spawn failed: #{e.message}")
|
||||
server.update(status: :failed)
|
||||
Activity.log(server.user, "spawn_failed", "Server", server.id, { error: e.message })
|
||||
|
||||
raise
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
19
app/models/job_log.rb
Normal file
19
app/models/job_log.rb
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
class JobLog < ApplicationRecord
|
||||
belongs_to :server, optional: true
|
||||
|
||||
enum :status, { pending: 0, running: 1, completed: 2, failed: 3 }
|
||||
|
||||
scope :recent, -> { order(created_at: :desc) }
|
||||
scope :for_server, ->(server_id) { where(server_id: server_id) }
|
||||
|
||||
def duration
|
||||
return nil unless started_at && finished_at
|
||||
finished_at - started_at
|
||||
end
|
||||
|
||||
def append_log(message)
|
||||
self.log_output ||= ""
|
||||
self.log_output += "[#{Time.current.strftime('%Y-%m-%d %H:%M:%S')}] #{message}\n"
|
||||
save
|
||||
end
|
||||
end
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
class Server < ApplicationRecord
|
||||
belongs_to :user
|
||||
belongs_to :server_template
|
||||
has_many :job_logs, dependent: :destroy
|
||||
|
||||
enum :status, { stopped: 0, starting: 1, running: 2, failed: 3 }
|
||||
|
||||
|
|
|
|||
47
app/views/job_logs/index.html.slim
Normal file
47
app/views/job_logs/index.html.slim
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
h1 Job Logs
|
||||
|
||||
- if params[:server_id]
|
||||
p
|
||||
= link_to "← Back to Server", server_path(params[:server_id]), class: "btn btn--secondary"
|
||||
= link_to "View All Jobs", job_logs_path, class: "btn btn--secondary"
|
||||
- else
|
||||
p.text-muted Showing all job logs for your servers
|
||||
|
||||
- if @job_logs.any?
|
||||
table.table
|
||||
thead
|
||||
tr
|
||||
th Job Class
|
||||
th Server
|
||||
th Status
|
||||
th Started
|
||||
th Duration
|
||||
th Actions
|
||||
tbody
|
||||
- @job_logs.each do |job_log|
|
||||
tr class="job-status-#{job_log.status}"
|
||||
td= job_log.job_class
|
||||
td
|
||||
- if job_log.server
|
||||
= link_to job_log.server.name, server_path(job_log.server)
|
||||
- else
|
||||
span.text-muted System
|
||||
td
|
||||
span class="badge badge--#{job_log.status}"
|
||||
= job_log.status.titleize
|
||||
td
|
||||
- if job_log.started_at
|
||||
= job_log.started_at.strftime("%Y-%m-%d %H:%M:%S")
|
||||
- else
|
||||
span.text-muted Pending
|
||||
td
|
||||
- if job_log.duration
|
||||
= "#{job_log.duration.round(2)}s"
|
||||
- elsif job_log.running?
|
||||
span.text-muted Running...
|
||||
- else
|
||||
span.text-muted -
|
||||
td
|
||||
= link_to "View Log", job_log_path(job_log), class: "btn btn--small"
|
||||
- else
|
||||
p.text-muted No job logs found.
|
||||
56
app/views/job_logs/show.html.slim
Normal file
56
app/views/job_logs/show.html.slim
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
h1 Job Log Details
|
||||
|
||||
.job-log-header
|
||||
.job-info
|
||||
h2= @job_log.job_class
|
||||
|
||||
dl.info-grid
|
||||
dt Status:
|
||||
dd
|
||||
span class="badge badge--#{@job_log.status}"
|
||||
= @job_log.status.titleize
|
||||
|
||||
- if @job_log.server
|
||||
dt Server:
|
||||
dd= link_to @job_log.server.name, server_path(@job_log.server)
|
||||
|
||||
dt Job ID:
|
||||
dd= @job_log.job_id || "N/A"
|
||||
|
||||
dt Started:
|
||||
dd
|
||||
- if @job_log.started_at
|
||||
= @job_log.started_at.strftime("%Y-%m-%d %H:%M:%S")
|
||||
- else
|
||||
span.text-muted Not started
|
||||
|
||||
dt Finished:
|
||||
dd
|
||||
- if @job_log.finished_at
|
||||
= @job_log.finished_at.strftime("%Y-%m-%d %H:%M:%S")
|
||||
- else
|
||||
span.text-muted -
|
||||
|
||||
- if @job_log.duration
|
||||
dt Duration:
|
||||
dd= "#{@job_log.duration.round(2)} seconds"
|
||||
|
||||
- if @job_log.error_message.present?
|
||||
dt Error:
|
||||
dd.error-message= @job_log.error_message
|
||||
|
||||
.job-arguments
|
||||
h3 Arguments
|
||||
pre.code-block= JSON.pretty_generate(JSON.parse(@job_log.arguments)) rescue @job_log.arguments
|
||||
|
||||
.job-log-output
|
||||
h3 Log Output
|
||||
- if @job_log.log_output.present?
|
||||
pre.log-output= @job_log.log_output
|
||||
- else
|
||||
p.text-muted No log output available.
|
||||
|
||||
.actions
|
||||
= link_to "← Back to Jobs", @job_log.server ? job_logs_path(server_id: @job_log.server_id) : job_logs_path, class: "btn btn--secondary"
|
||||
- if @job_log.server
|
||||
= link_to "View Server", server_path(@job_log.server), class: "btn btn--secondary"
|
||||
|
|
@ -28,6 +28,7 @@ html
|
|||
= link_to "Servers", servers_path
|
||||
= link_to "Templates", server_templates_path
|
||||
= link_to "Overlays", overlays_path
|
||||
= link_to "Jobs", job_logs_path
|
||||
span style="margin-left:auto;"
|
||||
= link_to "Logout", logout_path
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,31 @@
|
|||
= button_to "Delete Server", server_path(@server), method: :delete, data: { confirm: "Are you sure? This will stop and remove the server." }, class: "btn btn--danger"
|
||||
= link_to "Back to Servers", servers_path, class: "btn"
|
||||
|
||||
section.job-logs
|
||||
h3 Job History
|
||||
- if @server.job_logs.recent.limit(5).any?
|
||||
table.table.table--compact
|
||||
thead
|
||||
tr
|
||||
th Job
|
||||
th Status
|
||||
th Started
|
||||
th Duration
|
||||
th Actions
|
||||
tbody
|
||||
- @server.job_logs.recent.limit(5).each do |job_log|
|
||||
tr
|
||||
td= job_log.job_class
|
||||
td
|
||||
span class="badge badge--#{job_log.status}"
|
||||
= job_log.status.titleize
|
||||
td= job_log.started_at&.strftime("%H:%M:%S") || "-"
|
||||
td= job_log.duration ? "#{job_log.duration.round(1)}s" : "-"
|
||||
td= link_to "View", job_log_path(job_log), class: "btn btn--small"
|
||||
p= link_to "View All Jobs →", job_logs_path(server_id: @server.id), class: "link"
|
||||
- else
|
||||
p.text-muted No jobs executed yet.
|
||||
|
||||
section.logs
|
||||
h3 Live Logs
|
||||
#log-output
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ Rails.application.routes.draw do
|
|||
|
||||
resources :overlays, only: [ :index, :new, :create, :destroy ]
|
||||
|
||||
resources :job_logs, only: [ :index, :show ]
|
||||
|
||||
resources :servers do
|
||||
member do
|
||||
post :spawn
|
||||
|
|
|
|||
22
db/migrate/20260118200001_create_job_logs.rb
Normal file
22
db/migrate/20260118200001_create_job_logs.rb
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
class CreateJobLogs < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
create_table :job_logs do |t|
|
||||
t.string :job_class
|
||||
t.string :job_id
|
||||
t.text :arguments
|
||||
t.integer :server_id
|
||||
t.text :log_output
|
||||
t.integer :status, default: 0, null: false
|
||||
t.datetime :started_at
|
||||
t.datetime :finished_at
|
||||
t.text :error_message
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :job_logs, :server_id
|
||||
add_index :job_logs, :job_id
|
||||
add_index :job_logs, :status
|
||||
add_index :job_logs, :created_at
|
||||
end
|
||||
end
|
||||
20
db/schema.rb
generated
20
db/schema.rb
generated
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.1].define(version: 2026_01_18_200000) do
|
||||
ActiveRecord::Schema[8.1].define(version: 2026_01_18_200001) do
|
||||
create_table "activities", force: :cascade do |t|
|
||||
t.string "action", null: false
|
||||
t.datetime "created_at", null: false
|
||||
|
|
@ -23,6 +23,24 @@ ActiveRecord::Schema[8.1].define(version: 2026_01_18_200000) do
|
|||
t.index ["user_id"], name: "index_activities_on_user_id"
|
||||
end
|
||||
|
||||
create_table "job_logs", force: :cascade do |t|
|
||||
t.text "arguments"
|
||||
t.datetime "created_at", null: false
|
||||
t.text "error_message"
|
||||
t.datetime "finished_at"
|
||||
t.string "job_class"
|
||||
t.string "job_id"
|
||||
t.text "log_output"
|
||||
t.integer "server_id"
|
||||
t.datetime "started_at"
|
||||
t.integer "status", default: 0, null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["created_at"], name: "index_job_logs_on_created_at"
|
||||
t.index ["job_id"], name: "index_job_logs_on_job_id"
|
||||
t.index ["server_id"], name: "index_job_logs_on_server_id"
|
||||
t.index ["status"], name: "index_job_logs_on_status"
|
||||
end
|
||||
|
||||
create_table "overlays", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.text "description"
|
||||
|
|
|
|||
13
test/controllers/job_logs_controller_test.rb
Normal file
13
test/controllers/job_logs_controller_test.rb
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
require "test_helper"
|
||||
|
||||
class JobLogsControllerTest < ActionDispatch::IntegrationTest
|
||||
test "should get index" do
|
||||
get job_logs_index_url
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "should get show" do
|
||||
get job_logs_show_url
|
||||
assert_response :success
|
||||
end
|
||||
end
|
||||
23
test/fixtures/job_logs.yml
vendored
Normal file
23
test/fixtures/job_logs.yml
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||
|
||||
one:
|
||||
job_class: MyString
|
||||
job_id: MyString
|
||||
arguments: MyText
|
||||
server_id: 1
|
||||
log_output: MyText
|
||||
status: 1
|
||||
started_at: 2026-01-18 18:46:23
|
||||
finished_at: 2026-01-18 18:46:23
|
||||
error_message: MyText
|
||||
|
||||
two:
|
||||
job_class: MyString
|
||||
job_id: MyString
|
||||
arguments: MyText
|
||||
server_id: 1
|
||||
log_output: MyText
|
||||
status: 1
|
||||
started_at: 2026-01-18 18:46:23
|
||||
finished_at: 2026-01-18 18:46:23
|
||||
error_message: MyText
|
||||
7
test/models/job_log_test.rb
Normal file
7
test/models/job_log_test.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
require "test_helper"
|
||||
|
||||
class JobLogTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
Loading…
Reference in a new issue