rake_audit 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +38 -0
- data/LICENSE +7 -0
- data/README.md +235 -0
- data/app/controllers/rake_audit/application_controller.rb +53 -0
- data/app/controllers/rake_audit/dashboard_controller.rb +36 -0
- data/app/controllers/rake_audit/executions_controller.rb +39 -0
- data/app/models/rake_audit/task_execution.rb +33 -0
- data/app/views/layouts/rake_audit/application.html.erb +70 -0
- data/app/views/rake_audit/dashboard/index.html.erb +48 -0
- data/app/views/rake_audit/executions/index.html.erb +74 -0
- data/app/views/rake_audit/executions/show.html.erb +46 -0
- data/config/routes.rb +13 -0
- data/db/migrate/20260531120000_create_rake_task_executions.rb +62 -0
- data/lib/generators/rake_audit/install_generator.rb +42 -0
- data/lib/generators/rake_audit/templates/create_rake_task_executions.rb +55 -0
- data/lib/generators/rake_audit/templates/initializer.rb +17 -0
- data/lib/rake_audit/adapters/active_record_adapter.rb +73 -0
- data/lib/rake_audit/adapters/base.rb +94 -0
- data/lib/rake_audit/adapters/execution_record.rb +20 -0
- data/lib/rake_audit/adapters/mongo_adapter.rb +137 -0
- data/lib/rake_audit/adapters/redis_adapter.rb +174 -0
- data/lib/rake_audit/builders/task_execution_record_builder.rb +81 -0
- data/lib/rake_audit/configuration.rb +62 -0
- data/lib/rake_audit/execution_recorder.rb +78 -0
- data/lib/rake_audit/rails/engine.rb +20 -0
- data/lib/rake_audit/rails/railtie.rb +17 -0
- data/lib/rake_audit/record_not_found.rb +5 -0
- data/lib/rake_audit/task_execution_record.rb +46 -0
- data/lib/rake_audit/task_patch.rb +19 -0
- data/lib/rake_audit/version.rb +5 -0
- data/lib/rake_audit.rb +82 -0
- metadata +120 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9a179a34a78178c7a695d637e8ff9479d016258a2d7d69cb62e3b8ac33b557b7
|
|
4
|
+
data.tar.gz: 6c8cce01a8857dd1d3e50d32bdffe0a2f8d603460018d1aa70d59e01367496f4
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 50022c1e1011ad9143446c28f3b23a4563d1fc47251985e6bb81070f3f39793f898f42a83ffaf85d22cfad256fbbcef7e03b3e8fc163733fedaedabd4d80b620
|
|
7
|
+
data.tar.gz: 165a918cfeccc962b3f86342e3306b6efb8c54c7891103d674527192760442469888e553ffc38ec11f83e76c8a69433197bd7a1db2c057ef65f551d360fa9bfe
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-06-03
|
|
11
|
+
|
|
12
|
+
Initial public release.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Automatic Rake auditing** — a prepended `Rake::Task#execute` hook records the
|
|
17
|
+
task name, arguments, start/finish times, duration, status (success/failure), and
|
|
18
|
+
error details for every Rake task execution, without altering task behavior.
|
|
19
|
+
- **Pluggable storage adapters** built on a common `RakeAudit::Adapters::Base`:
|
|
20
|
+
- `ActiveRecordAdapter` — persists executions to a `rake_task_executions` table.
|
|
21
|
+
- `RedisAdapter` — stores executions as JSON in the `rake_audit:executions` list.
|
|
22
|
+
- `MongoAdapter` — stores executions in the `rake_task_executions` collection.
|
|
23
|
+
- Custom adapters are supported by subclassing `Base` and implementing `#save`.
|
|
24
|
+
- **Configurable capture** of hostname, PID, Ruby version, and Rails environment,
|
|
25
|
+
plus a configurable logger for adapter save failures.
|
|
26
|
+
- **Rails Web UI** (mountable engine) providing:
|
|
27
|
+
- A paginated, filterable execution list (Kaminari-backed).
|
|
28
|
+
- A dashboard with aggregate stats (totals, success/failure counts, average
|
|
29
|
+
duration, top failed tasks).
|
|
30
|
+
- A per-execution detail view.
|
|
31
|
+
- Optional access control via a configurable `authenticate_with` lambda.
|
|
32
|
+
- **Rails install generator** (`rails generate rake_audit:install`) that copies an
|
|
33
|
+
initializer and a timestamped ActiveRecord migration.
|
|
34
|
+
- **Rails-free operation** — the gem loads and records outside Rails; Kaminari's
|
|
35
|
+
ActiveRecord/ActionView integrations stay inert until those libraries are present.
|
|
36
|
+
|
|
37
|
+
[Unreleased]: https://github.com/eraxel-dev/rake_audit/compare/v0.1.0...HEAD
|
|
38
|
+
[0.1.0]: https://github.com/eraxel-dev/rake_audit/releases/tag/v0.1.0
|
data/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright (c) 2026 Eraxel Dev
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# Rake Audit
|
|
2
|
+
|
|
3
|
+
**Full Audit Trail for Every Rake Execution**
|
|
4
|
+
|
|
5
|
+
RakeAudit automatically records who ran what, when it started, when it finished, how long it took, and whether it succeeded or failed.
|
|
6
|
+
|
|
7
|
+
Built for modern Rails applications, it supports pluggable storage adapters including ActiveRecord, Redis, MongoDB, and custom backends.
|
|
8
|
+
|
|
9
|
+
Gain a complete historical record of your operational tasks without touching your existing codebase.
|
|
10
|
+
|
|
11
|
+
## Table of Contents
|
|
12
|
+
|
|
13
|
+
- [Installation](#installation)
|
|
14
|
+
- [Setup](#setup)
|
|
15
|
+
- [Rails (ActiveRecord)](#rails-activerecord)
|
|
16
|
+
- [Redis](#redis)
|
|
17
|
+
- [MongoDB](#mongodb)
|
|
18
|
+
- [Plain Ruby / Standalone Rake](#plain-ruby--standalone-rake)
|
|
19
|
+
- [Configuration](#configuration)
|
|
20
|
+
- [Web UI](#web-ui)
|
|
21
|
+
- [Custom Adapter](#custom-adapter)
|
|
22
|
+
- [Example Usages](#example-usages)
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
Add the gem to your `Gemfile`:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
gem 'rake_audit'
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Then run:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
bundle install
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Or install directly:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
gem install rake_audit
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Setup
|
|
49
|
+
|
|
50
|
+
### Rails (ActiveRecord)
|
|
51
|
+
|
|
52
|
+
Run the install generator to create the initializer and database migration:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
rails generate rake_audit:install
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This copies `config/initializers/rake_audit.rb` and a timestamped migration to `db/migrate/`. Run the migration:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
rails db:migrate
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Then enable the ActiveRecord adapter in `config/initializers/rake_audit.rb`:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
RakeAudit.configure do |config|
|
|
68
|
+
config.adapter = RakeAudit::Adapters::ActiveRecordAdapter.new
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Redis
|
|
73
|
+
|
|
74
|
+
Provide a connected `Redis` client:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
require 'redis'
|
|
78
|
+
|
|
79
|
+
RakeAudit.configure do |config|
|
|
80
|
+
config.adapter = RakeAudit::Adapters::RedisAdapter.new(
|
|
81
|
+
client: Redis.new(url: ENV['REDIS_URL'])
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Execution records are stored as JSON in the `rake_audit:executions` list key.
|
|
87
|
+
|
|
88
|
+
### MongoDB
|
|
89
|
+
|
|
90
|
+
Provide a connected `Mongo::Client`:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
require 'mongo'
|
|
94
|
+
|
|
95
|
+
RakeAudit.configure do |config|
|
|
96
|
+
config.adapter = RakeAudit::Adapters::MongoAdapter.new(
|
|
97
|
+
client: Mongo::Client.new(ENV['MONGODB_URI'])
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Records are stored in the `rake_task_executions` collection.
|
|
103
|
+
|
|
104
|
+
### Plain Ruby / Standalone Rake
|
|
105
|
+
|
|
106
|
+
RakeAudit works outside Rails. Require the gem and configure an adapter before tasks run:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
require 'rake_audit'
|
|
110
|
+
|
|
111
|
+
RakeAudit.configure do |config|
|
|
112
|
+
config.adapter = MyCustomAdapter.new
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
RakeAudit.install!
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Configuration
|
|
121
|
+
|
|
122
|
+
All options with their defaults:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
RakeAudit.configure do |config|
|
|
126
|
+
# Storage adapter — required to enable recording.
|
|
127
|
+
config.adapter = RakeAudit::Adapters::ActiveRecordAdapter.new
|
|
128
|
+
|
|
129
|
+
# Logger for adapter save failures (defaults to Rails.logger or $stdout).
|
|
130
|
+
config.logger = Rails.logger
|
|
131
|
+
|
|
132
|
+
# Fields captured on each execution record.
|
|
133
|
+
config.capture_hostname = true # machine hostname
|
|
134
|
+
config.capture_pid = true # process ID
|
|
135
|
+
config.capture_ruby_version = true # Ruby version string
|
|
136
|
+
config.capture_rails_env = true # Rails.env value
|
|
137
|
+
|
|
138
|
+
# Web UI (see below).
|
|
139
|
+
config.web_ui_enabled = true
|
|
140
|
+
config.authenticate_with = ->(controller) { controller.authenticate_admin! }
|
|
141
|
+
end
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Without an adapter, RakeAudit loads silently and records nothing.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Web UI
|
|
149
|
+
|
|
150
|
+
Mount the engine in `config/routes.rb` to enable the built-in dashboard and execution list:
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
Rails.application.routes.draw do
|
|
154
|
+
mount RakeAudit::Engine, at: '/rake_audit'
|
|
155
|
+
end
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
| Path | Description |
|
|
159
|
+
|---|---|
|
|
160
|
+
| `/rake_audit` | Paginated execution list with filters |
|
|
161
|
+
| `/rake_audit/dashboard` | Aggregate stats (total, success/failure counts, avg duration) |
|
|
162
|
+
| `/rake_audit/executions/:id` | Detail view for a single execution |
|
|
163
|
+
|
|
164
|
+
To restrict access, set `authenticate_with` to a lambda that calls your authentication helper:
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
config.authenticate_with = ->(controller) { controller.authenticate_admin! }
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Custom Adapter
|
|
173
|
+
|
|
174
|
+
Subclass `RakeAudit::Adapters::Base` and implement `#save`. Implement the remaining query methods to support the Web UI:
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
class MyAdapter < RakeAudit::Adapters::Base
|
|
178
|
+
def save(record)
|
|
179
|
+
# record is a RakeAudit::TaskExecutionRecord
|
|
180
|
+
# record.to_h returns a plain hash of all fields
|
|
181
|
+
MyStore.insert(record.to_h)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Optional — required only when using the Web UI.
|
|
185
|
+
def query(filters: {}, page: nil, per_page: 25) = raise NotImplementedError
|
|
186
|
+
def find(id) = raise NotImplementedError
|
|
187
|
+
def count = raise NotImplementedError
|
|
188
|
+
def count_by_status(status) = raise NotImplementedError
|
|
189
|
+
def average_duration_ms = raise NotImplementedError
|
|
190
|
+
def top_failed_tasks(limit: 10) = raise NotImplementedError
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
RakeAudit.configure { |c| c.adapter = MyAdapter.new }
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Example Usages
|
|
199
|
+
|
|
200
|
+
**Query executions directly (ActiveRecord adapter)**
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
# All executions for a task, newest first.
|
|
204
|
+
RakeAudit::TaskExecution.where(task_name: 'db:migrate').order(started_at: :desc)
|
|
205
|
+
|
|
206
|
+
# Failed executions in the last 24 hours.
|
|
207
|
+
RakeAudit::TaskExecution.where(status: 'failure')
|
|
208
|
+
.where(started_at: 24.hours.ago..)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Check stats programmatically**
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
adapter = RakeAudit.config.adapter
|
|
215
|
+
stats = adapter.stats
|
|
216
|
+
# => { total: 1482, success_count: 1470, failure_count: 12,
|
|
217
|
+
# average_duration_ms: 843.2, top_failed_tasks: [["db:seed", 7], ...] }
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**Reset configuration in tests**
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
RSpec.configure do |config|
|
|
224
|
+
config.after { RakeAudit.reset_config! }
|
|
225
|
+
end
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Disable the Web UI for API-only apps**
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
RakeAudit.configure do |config|
|
|
232
|
+
config.adapter = RakeAudit::Adapters::ActiveRecordAdapter.new
|
|
233
|
+
config.web_ui_enabled = false
|
|
234
|
+
end
|
|
235
|
+
```
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RakeAudit
|
|
4
|
+
# Base controller for every RakeAudit Web UI page.
|
|
5
|
+
#
|
|
6
|
+
# It installs the pluggable authentication hook as a +before_action+. When the
|
|
7
|
+
# host application has configured {RakeAudit::Configuration#authenticate_with}
|
|
8
|
+
# with a callable, that callable is run in the controller instance's context on
|
|
9
|
+
# every request (so it may call host helpers such as +authenticate_admin!+ or
|
|
10
|
+
# +redirect_to+). When +authenticate_with+ is +nil+, the hook is a no-op and
|
|
11
|
+
# all pages are publicly accessible.
|
|
12
|
+
class ApplicationController < ActionController::Base
|
|
13
|
+
protect_from_forgery with: :exception
|
|
14
|
+
|
|
15
|
+
before_action :authenticate!
|
|
16
|
+
rescue_from RakeAudit::RecordNotFound, with: :record_not_found
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
# Run the configured authentication callable, if any, in the context of this
|
|
21
|
+
# controller instance. The controller is also passed as the block argument so
|
|
22
|
+
# procs may be written as +->(controller) { ... }+ or use +self+ directly.
|
|
23
|
+
#
|
|
24
|
+
# @return [void]
|
|
25
|
+
def authenticate!
|
|
26
|
+
callable = RakeAudit.config.authenticate_with
|
|
27
|
+
return unless callable
|
|
28
|
+
|
|
29
|
+
instance_exec(self, &callable)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns the configured adapter. Falls back to a fresh ActiveRecordAdapter
|
|
33
|
+
# when none is set so existing apps keep working without explicit configuration.
|
|
34
|
+
#
|
|
35
|
+
# @return [RakeAudit::Adapters::Base]
|
|
36
|
+
def web_adapter
|
|
37
|
+
RakeAudit.config.adapter || default_web_adapter
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def default_web_adapter
|
|
41
|
+
require 'rake_audit/adapters/active_record_adapter'
|
|
42
|
+
RakeAudit.config.logger.warn(
|
|
43
|
+
'[RakeAudit] config.adapter is nil — falling back to ActiveRecordAdapter. ' \
|
|
44
|
+
'Set RakeAudit.configure { |c| c.adapter = ... } in your initializer to silence this warning.'
|
|
45
|
+
)
|
|
46
|
+
RakeAudit::Adapters::ActiveRecordAdapter.new
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def record_not_found
|
|
50
|
+
head :not_found
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RakeAudit
|
|
4
|
+
# Renders the dashboard: six aggregate metrics plus the ten task names with
|
|
5
|
+
# the most failures. All data access is delegated to +web_adapter+ so the
|
|
6
|
+
# controller is backend-agnostic (ActiveRecord, Redis, Mongo, …).
|
|
7
|
+
class DashboardController < ApplicationController
|
|
8
|
+
# Compute the dashboard aggregates and expose them as instance variables.
|
|
9
|
+
# All five metrics are fetched via a single +web_adapter.stats+ call so
|
|
10
|
+
# adapters can compute them in one pass (e.g. one Redis LRANGE instead of five).
|
|
11
|
+
#
|
|
12
|
+
# @return [void]
|
|
13
|
+
def index
|
|
14
|
+
s = web_adapter.stats
|
|
15
|
+
@total = s[:total]
|
|
16
|
+
@success_count = s[:success_count]
|
|
17
|
+
@failure_count = s[:failure_count]
|
|
18
|
+
@failure_rate = failure_rate(@failure_count, @total)
|
|
19
|
+
@average_duration_ms = s[:average_duration_ms]
|
|
20
|
+
@top_failed_tasks = s[:top_failed_tasks]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# Percentage of executions that failed, guarding against division by zero.
|
|
26
|
+
#
|
|
27
|
+
# @param failures [Integer]
|
|
28
|
+
# @param total [Integer]
|
|
29
|
+
# @return [Float]
|
|
30
|
+
def failure_rate(failures, total)
|
|
31
|
+
return 0.0 if total.zero?
|
|
32
|
+
|
|
33
|
+
failures / total.to_f * 100
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RakeAudit
|
|
4
|
+
# Lists recorded task executions (with filtering + pagination) and shows the
|
|
5
|
+
# detail of a single execution. All data access is delegated to +web_adapter+
|
|
6
|
+
# so the controller is backend-agnostic (ActiveRecord, Redis, Mongo, …).
|
|
7
|
+
class ExecutionsController < ApplicationController
|
|
8
|
+
# Newest-first, filtered, paginated list of executions.
|
|
9
|
+
#
|
|
10
|
+
# @return [void]
|
|
11
|
+
def index
|
|
12
|
+
@executions = web_adapter.query(filters: filter_params, page: params[:page])
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Detail of one execution, looked up by id.
|
|
16
|
+
#
|
|
17
|
+
# @return [void]
|
|
18
|
+
def show
|
|
19
|
+
@execution = web_adapter.find(params[:id])
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# Build a filter hash from request params, omitting any blank values so
|
|
25
|
+
# adapters can safely check +filters.key?(col)+ without blank-value guards.
|
|
26
|
+
#
|
|
27
|
+
# @return [Hash]
|
|
28
|
+
def filter_params
|
|
29
|
+
{
|
|
30
|
+
task_name: params[:task_name],
|
|
31
|
+
status: params[:status],
|
|
32
|
+
hostname: params[:hostname],
|
|
33
|
+
rails_env: params[:rails_env],
|
|
34
|
+
from: params[:from],
|
|
35
|
+
to: params[:to]
|
|
36
|
+
}.reject { |_, v| v.blank? }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RakeAudit
|
|
4
|
+
# ActiveRecord model backing the +rake_task_executions+ table.
|
|
5
|
+
#
|
|
6
|
+
# This is the persistence target of {RakeAudit::Adapters::ActiveRecordAdapter},
|
|
7
|
+
# which calls +TaskExecution.create!(record.to_h)+ with the twelve fields a
|
|
8
|
+
# {RakeAudit::TaskExecutionRecord} serializes. The four columns that are
|
|
9
|
+
# always populated for a completed execution carry presence validations so a
|
|
10
|
+
# malformed record fails loudly rather than persisting a partial row.
|
|
11
|
+
#
|
|
12
|
+
# When the host application defines an +ApplicationRecord+ the model inherits
|
|
13
|
+
# from it (picking up the app's connection and conventions); otherwise it
|
|
14
|
+
# falls back to +ActiveRecord::Base+ so the gem also works in a plain
|
|
15
|
+
# ActiveRecord setup without Rails.
|
|
16
|
+
class TaskExecution < (defined?(ApplicationRecord) ? ApplicationRecord : ActiveRecord::Base)
|
|
17
|
+
self.table_name = 'rake_task_executions'
|
|
18
|
+
|
|
19
|
+
# Treat +arguments+ as JSON on every backend so a Hash round-trips to a Hash.
|
|
20
|
+
# On PostgreSQL/MySQL the column is natively json(b); on SQLite it is +text+,
|
|
21
|
+
# where without an explicit JSON type a Hash would be stored via Ruby's
|
|
22
|
+
# +to_s+ and read back as a String. +ActiveRecord::Type::Json+ casts Hash to
|
|
23
|
+
# JSON on write and back on read for both column kinds, needs no database
|
|
24
|
+
# connection at load time, and never raises — unlike +serialize+, which
|
|
25
|
+
# rejects a natively JSON-typed column.
|
|
26
|
+
attribute :arguments, ActiveRecord::Type::Json.new
|
|
27
|
+
|
|
28
|
+
validates :task_name, presence: true
|
|
29
|
+
validates :status, presence: true
|
|
30
|
+
validates :started_at, presence: true
|
|
31
|
+
validates :finished_at, presence: true
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>RakeAudit</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { color-scheme: light dark; }
|
|
9
|
+
* { box-sizing: border-box; }
|
|
10
|
+
body {
|
|
11
|
+
margin: 0;
|
|
12
|
+
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
13
|
+
line-height: 1.5;
|
|
14
|
+
color: #1f2933;
|
|
15
|
+
background: #f5f7fa;
|
|
16
|
+
}
|
|
17
|
+
header.rake-audit-bar {
|
|
18
|
+
background: #1f2933;
|
|
19
|
+
color: #fff;
|
|
20
|
+
padding: 0.75rem 1.5rem;
|
|
21
|
+
display: flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
gap: 1.5rem;
|
|
24
|
+
}
|
|
25
|
+
header.rake-audit-bar a { color: #cbd2d9; text-decoration: none; font-weight: 600; }
|
|
26
|
+
header.rake-audit-bar a:hover { color: #fff; }
|
|
27
|
+
header.rake-audit-bar .brand { color: #fff; font-size: 1.1rem; }
|
|
28
|
+
main { padding: 1.5rem; max-width: 1100px; margin: 0 auto; }
|
|
29
|
+
h1 { font-size: 1.4rem; margin-top: 0; }
|
|
30
|
+
table { width: 100%; border-collapse: collapse; background: #fff; }
|
|
31
|
+
th, td { text-align: left; padding: 0.5rem 0.75rem; border-bottom: 1px solid #e4e7eb; }
|
|
32
|
+
th { background: #f0f4f8; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.03em; }
|
|
33
|
+
.cards { display: flex; flex-wrap: wrap; gap: 1rem; margin-bottom: 1.5rem; }
|
|
34
|
+
.card {
|
|
35
|
+
background: #fff;
|
|
36
|
+
border: 1px solid #e4e7eb;
|
|
37
|
+
border-radius: 8px;
|
|
38
|
+
padding: 1rem 1.25rem;
|
|
39
|
+
min-width: 150px;
|
|
40
|
+
flex: 1;
|
|
41
|
+
}
|
|
42
|
+
.card .label { font-size: 0.75rem; text-transform: uppercase; color: #7b8794; }
|
|
43
|
+
.card .value { font-size: 1.6rem; font-weight: 700; }
|
|
44
|
+
form.filters { background: #fff; border: 1px solid #e4e7eb; border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem; }
|
|
45
|
+
form.filters .row { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: flex-end; }
|
|
46
|
+
form.filters label { display: flex; flex-direction: column; font-size: 0.75rem; color: #52606d; gap: 0.25rem; }
|
|
47
|
+
form.filters input, form.filters select { padding: 0.35rem 0.5rem; border: 1px solid #cbd2d9; border-radius: 4px; }
|
|
48
|
+
form.filters button { padding: 0.45rem 1rem; border: 0; border-radius: 4px; background: #2563eb; color: #fff; cursor: pointer; }
|
|
49
|
+
.status-success { color: #057a55; font-weight: 600; }
|
|
50
|
+
.status-failure { color: #c81e1e; font-weight: 600; }
|
|
51
|
+
.section { background: #fff; border: 1px solid #e4e7eb; border-radius: 8px; padding: 1rem 1.25rem; margin-bottom: 1rem; }
|
|
52
|
+
.section h2 { font-size: 1rem; margin-top: 0; }
|
|
53
|
+
dl { display: grid; grid-template-columns: max-content 1fr; gap: 0.35rem 1rem; margin: 0; }
|
|
54
|
+
dt { font-weight: 600; color: #52606d; }
|
|
55
|
+
pre { background: #f0f4f8; padding: 0.75rem; border-radius: 4px; overflow-x: auto; margin: 0; }
|
|
56
|
+
.pagination { margin-top: 1rem; }
|
|
57
|
+
.pagination a, .pagination span { padding: 0.25rem 0.5rem; }
|
|
58
|
+
</style>
|
|
59
|
+
</head>
|
|
60
|
+
<body>
|
|
61
|
+
<header class="rake-audit-bar">
|
|
62
|
+
<span class="brand">RakeAudit</span>
|
|
63
|
+
<%= link_to "Executions", executions_path %>
|
|
64
|
+
<%= link_to "Dashboard", dashboard_path %>
|
|
65
|
+
</header>
|
|
66
|
+
<main>
|
|
67
|
+
<%= yield %>
|
|
68
|
+
</main>
|
|
69
|
+
</body>
|
|
70
|
+
</html>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<h1>Dashboard</h1>
|
|
2
|
+
|
|
3
|
+
<section class="cards">
|
|
4
|
+
<div class="card">
|
|
5
|
+
<div class="label">Total</div>
|
|
6
|
+
<div class="value"><%= @total %></div>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="card">
|
|
9
|
+
<div class="label">Success</div>
|
|
10
|
+
<div class="value"><%= @success_count %></div>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="card">
|
|
13
|
+
<div class="label">Failure</div>
|
|
14
|
+
<div class="value"><%= @failure_count %></div>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="card">
|
|
17
|
+
<div class="label">Failure Rate</div>
|
|
18
|
+
<div class="value"><%= format("%.1f%%", @failure_rate) %></div>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="card">
|
|
21
|
+
<div class="label">Avg Duration (ms)</div>
|
|
22
|
+
<div class="value"><%= @average_duration_ms ? format("%.1f", @average_duration_ms) : "—" %></div>
|
|
23
|
+
</div>
|
|
24
|
+
</section>
|
|
25
|
+
|
|
26
|
+
<section class="section">
|
|
27
|
+
<h2>Top 10 Failed Tasks</h2>
|
|
28
|
+
<% if @top_failed_tasks.any? %>
|
|
29
|
+
<table>
|
|
30
|
+
<thead>
|
|
31
|
+
<tr>
|
|
32
|
+
<th>Task Name</th>
|
|
33
|
+
<th>Failure Count</th>
|
|
34
|
+
</tr>
|
|
35
|
+
</thead>
|
|
36
|
+
<tbody>
|
|
37
|
+
<% @top_failed_tasks.each do |task_name, failure_count| %>
|
|
38
|
+
<tr>
|
|
39
|
+
<td><%= task_name %></td>
|
|
40
|
+
<td><%= failure_count %></td>
|
|
41
|
+
</tr>
|
|
42
|
+
<% end %>
|
|
43
|
+
</tbody>
|
|
44
|
+
</table>
|
|
45
|
+
<% else %>
|
|
46
|
+
<p>No failed tasks recorded.</p>
|
|
47
|
+
<% end %>
|
|
48
|
+
</section>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<h1>Executions</h1>
|
|
2
|
+
|
|
3
|
+
<%= form_with url: executions_path, method: :get, class: "filters" do |f| %>
|
|
4
|
+
<div class="row">
|
|
5
|
+
<label>
|
|
6
|
+
Task Name
|
|
7
|
+
<%= f.text_field :task_name, value: params[:task_name] %>
|
|
8
|
+
</label>
|
|
9
|
+
<label>
|
|
10
|
+
Status
|
|
11
|
+
<%= f.select :status,
|
|
12
|
+
options_for_select(
|
|
13
|
+
[["All", ""], ["Success", "success"], ["Failure", "failure"]],
|
|
14
|
+
params[:status]
|
|
15
|
+
) %>
|
|
16
|
+
</label>
|
|
17
|
+
<label>
|
|
18
|
+
Hostname
|
|
19
|
+
<%= f.text_field :hostname, value: params[:hostname] %>
|
|
20
|
+
</label>
|
|
21
|
+
<label>
|
|
22
|
+
Rails Env
|
|
23
|
+
<%= f.text_field :rails_env, value: params[:rails_env] %>
|
|
24
|
+
</label>
|
|
25
|
+
<label>
|
|
26
|
+
From
|
|
27
|
+
<%= f.date_field :from, value: params[:from] %>
|
|
28
|
+
</label>
|
|
29
|
+
<label>
|
|
30
|
+
To
|
|
31
|
+
<%= f.date_field :to, value: params[:to] %>
|
|
32
|
+
</label>
|
|
33
|
+
<button type="submit">Filter</button>
|
|
34
|
+
</div>
|
|
35
|
+
<% end %>
|
|
36
|
+
|
|
37
|
+
<table>
|
|
38
|
+
<thead>
|
|
39
|
+
<tr>
|
|
40
|
+
<th>Task Name</th>
|
|
41
|
+
<th>Status</th>
|
|
42
|
+
<th>Duration (ms)</th>
|
|
43
|
+
<th>Started At</th>
|
|
44
|
+
<th>Hostname</th>
|
|
45
|
+
<th>Rails Env</th>
|
|
46
|
+
<th></th>
|
|
47
|
+
</tr>
|
|
48
|
+
</thead>
|
|
49
|
+
<tbody>
|
|
50
|
+
<% if @executions.any? %>
|
|
51
|
+
<% @executions.each do |execution| %>
|
|
52
|
+
<tr>
|
|
53
|
+
<td><%= execution.task_name %></td>
|
|
54
|
+
<td class="status-<%= execution.status %>"><%= execution.status %></td>
|
|
55
|
+
<td><%= execution.duration_ms %></td>
|
|
56
|
+
<td><%= execution.started_at %></td>
|
|
57
|
+
<td><%= execution.hostname %></td>
|
|
58
|
+
<td><%= execution.rails_env %></td>
|
|
59
|
+
<td><%= link_to "View", execution_path(execution) %></td>
|
|
60
|
+
</tr>
|
|
61
|
+
<% end %>
|
|
62
|
+
<% else %>
|
|
63
|
+
<tr>
|
|
64
|
+
<td colspan="7">No executions found.</td>
|
|
65
|
+
</tr>
|
|
66
|
+
<% end %>
|
|
67
|
+
</tbody>
|
|
68
|
+
</table>
|
|
69
|
+
|
|
70
|
+
<% if @executions.respond_to?(:current_page) %>
|
|
71
|
+
<div class="pagination">
|
|
72
|
+
<%= paginate @executions if respond_to?(:paginate) %>
|
|
73
|
+
</div>
|
|
74
|
+
<% end %>
|