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
|
# Most jobs are safe to ignore if the underlying records are no longer available
|
||||||
# discard_on ActiveJob::DeserializationError
|
# 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
|
end
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,32 @@ class SpawnServerJob < ApplicationJob
|
||||||
|
|
||||||
def perform(server_id)
|
def perform(server_id)
|
||||||
server = Server.find(server_id)
|
server = Server.find(server_id)
|
||||||
|
log "Found server: #{server.name}"
|
||||||
|
|
||||||
begin
|
begin
|
||||||
|
log "Starting server spawn process..."
|
||||||
L4dServer::Launcher.spawn(server)
|
L4dServer::Launcher.spawn(server)
|
||||||
|
log "Server spawned successfully"
|
||||||
|
|
||||||
server.update(status: :starting)
|
server.update(status: :starting)
|
||||||
|
log "Server status updated to starting"
|
||||||
|
|
||||||
# Generate a random RCON password for health checks
|
# 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 })
|
Activity.log(server.user, "spawned_success", "Server", server.id, { name: server.name })
|
||||||
|
log "Activity logged"
|
||||||
rescue StandardError => e
|
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}")
|
Rails.logger.error("Server spawn failed: #{e.message}")
|
||||||
server.update(status: :failed)
|
server.update(status: :failed)
|
||||||
Activity.log(server.user, "spawn_failed", "Server", server.id, { error: e.message })
|
Activity.log(server.user, "spawn_failed", "Server", server.id, { error: e.message })
|
||||||
|
|
||||||
|
raise
|
||||||
end
|
end
|
||||||
end
|
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
|
class Server < ApplicationRecord
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
belongs_to :server_template
|
belongs_to :server_template
|
||||||
|
has_many :job_logs, dependent: :destroy
|
||||||
|
|
||||||
enum :status, { stopped: 0, starting: 1, running: 2, failed: 3 }
|
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 "Servers", servers_path
|
||||||
= link_to "Templates", server_templates_path
|
= link_to "Templates", server_templates_path
|
||||||
= link_to "Overlays", overlays_path
|
= link_to "Overlays", overlays_path
|
||||||
|
= link_to "Jobs", job_logs_path
|
||||||
span style="margin-left:auto;"
|
span style="margin-left:auto;"
|
||||||
= link_to "Logout", logout_path
|
= 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"
|
= 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"
|
= 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
|
section.logs
|
||||||
h3 Live Logs
|
h3 Live Logs
|
||||||
#log-output
|
#log-output
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ Rails.application.routes.draw do
|
||||||
|
|
||||||
resources :overlays, only: [ :index, :new, :create, :destroy ]
|
resources :overlays, only: [ :index, :new, :create, :destroy ]
|
||||||
|
|
||||||
|
resources :job_logs, only: [ :index, :show ]
|
||||||
|
|
||||||
resources :servers do
|
resources :servers do
|
||||||
member do
|
member do
|
||||||
post :spawn
|
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.
|
# 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|
|
create_table "activities", force: :cascade do |t|
|
||||||
t.string "action", null: false
|
t.string "action", null: false
|
||||||
t.datetime "created_at", 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"
|
t.index ["user_id"], name: "index_activities_on_user_id"
|
||||||
end
|
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|
|
create_table "overlays", force: :cascade do |t|
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.text "description"
|
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