solid_observer 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 +58 -0
- data/LICENSE.txt +21 -0
- data/README.md +347 -0
- data/app/jobs/solid_observer/cleanup_job.rb +12 -0
- data/app/models/solid_observer/queue_event.rb +23 -0
- data/app/models/solid_observer/queue_metric.rb +14 -0
- data/app/models/solid_observer/storage_info.rb +36 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/db/migrate/20260115000001_create_solid_observer_queue_events.rb +21 -0
- data/db/migrate/20260115000002_create_solid_observer_metrics.rb +16 -0
- data/db/migrate/20260115000003_create_solid_observer_storage_info.rb +13 -0
- data/lib/generators/solid_observer/install_generator.rb +72 -0
- data/lib/generators/solid_observer/templates/initializer.rb.tt +57 -0
- data/lib/solid_observer/base_event.rb +10 -0
- data/lib/solid_observer/base_metric.rb +59 -0
- data/lib/solid_observer/cli/base.rb +98 -0
- data/lib/solid_observer/cli/jobs.rb +195 -0
- data/lib/solid_observer/cli/status.rb +59 -0
- data/lib/solid_observer/cli/storage.rb +114 -0
- data/lib/solid_observer/configuration.rb +125 -0
- data/lib/solid_observer/correlation_id_resolver.rb +62 -0
- data/lib/solid_observer/engine.rb +60 -0
- data/lib/solid_observer/queue_event_buffer.rb +80 -0
- data/lib/solid_observer/queue_stats.rb +89 -0
- data/lib/solid_observer/services/cleanup_storage.rb +94 -0
- data/lib/solid_observer/services/flush_event_buffer.rb +65 -0
- data/lib/solid_observer/services/record_event.rb +96 -0
- data/lib/solid_observer/subscriber.rb +96 -0
- data/lib/solid_observer/version.rb +7 -0
- data/lib/solid_observer.rb +40 -0
- data/lib/tasks/solid_observer.rake +155 -0
- metadata +93 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidObserver
|
|
4
|
+
# Subscribes to ActiveSupport::Notifications for ActiveJob events.
|
|
5
|
+
#
|
|
6
|
+
# Monitors job lifecycle events (enqueue, perform, retry_stopped, discard)
|
|
7
|
+
# and records them through the event buffer for observability.
|
|
8
|
+
#
|
|
9
|
+
# @example Subscribe to job events
|
|
10
|
+
# SolidObserver::Subscriber.subscribe!
|
|
11
|
+
class Subscriber
|
|
12
|
+
EVENTS = %w[
|
|
13
|
+
enqueue.active_job
|
|
14
|
+
perform.active_job
|
|
15
|
+
retry_stopped.active_job
|
|
16
|
+
discard.active_job
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
def subscribe!
|
|
21
|
+
return unless SolidObserver.config.observe_queue
|
|
22
|
+
return if subscribed?
|
|
23
|
+
|
|
24
|
+
@subscriptions = []
|
|
25
|
+
@subscriptions << subscribe_to_enqueue
|
|
26
|
+
@subscriptions << subscribe_to_perform
|
|
27
|
+
@subscriptions << subscribe_to_retry_stopped
|
|
28
|
+
@subscriptions << subscribe_to_discard
|
|
29
|
+
@subscriptions.compact!
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def unsubscribe!
|
|
33
|
+
return unless @subscriptions
|
|
34
|
+
|
|
35
|
+
@subscriptions.each do |subscription|
|
|
36
|
+
ActiveSupport::Notifications.unsubscribe(subscription)
|
|
37
|
+
end
|
|
38
|
+
@subscriptions = []
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def subscribed?
|
|
42
|
+
!!@subscriptions&.any?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def subscribe_to_enqueue
|
|
48
|
+
ActiveSupport::Notifications.subscribe("enqueue.active_job") do |*args|
|
|
49
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
50
|
+
Services::RecordEvent.call(
|
|
51
|
+
event: event,
|
|
52
|
+
event_type: "job_enqueued",
|
|
53
|
+
buffer: QueueEventBuffer.instance,
|
|
54
|
+
metric_name: "jobs_enqueued"
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def subscribe_to_perform
|
|
60
|
+
ActiveSupport::Notifications.subscribe("perform.active_job") do |*args|
|
|
61
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
62
|
+
Services::RecordEvent.call(
|
|
63
|
+
event: event,
|
|
64
|
+
event_type: "job_completed",
|
|
65
|
+
buffer: QueueEventBuffer.instance,
|
|
66
|
+
metric_name: "jobs_completed"
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def subscribe_to_retry_stopped
|
|
72
|
+
ActiveSupport::Notifications.subscribe("retry_stopped.active_job") do |*args|
|
|
73
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
74
|
+
Services::RecordEvent.call(
|
|
75
|
+
event: event,
|
|
76
|
+
event_type: "job_failed",
|
|
77
|
+
buffer: QueueEventBuffer.instance,
|
|
78
|
+
metric_name: "jobs_failed"
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def subscribe_to_discard
|
|
84
|
+
ActiveSupport::Notifications.subscribe("discard.active_job") do |*args|
|
|
85
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
86
|
+
Services::RecordEvent.call(
|
|
87
|
+
event: event,
|
|
88
|
+
event_type: "job_discarded",
|
|
89
|
+
buffer: QueueEventBuffer.instance,
|
|
90
|
+
metric_name: "jobs_discarded"
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "solid_observer/version"
|
|
4
|
+
require_relative "solid_observer/configuration"
|
|
5
|
+
require_relative "solid_observer/correlation_id_resolver"
|
|
6
|
+
require_relative "solid_observer/base_event" if defined?(ActiveRecord)
|
|
7
|
+
require_relative "solid_observer/base_metric" if defined?(ActiveRecord)
|
|
8
|
+
require_relative "solid_observer/services/record_event" if defined?(ActiveRecord)
|
|
9
|
+
require_relative "solid_observer/services/flush_event_buffer" if defined?(ActiveRecord)
|
|
10
|
+
require_relative "solid_observer/services/cleanup_storage" if defined?(ActiveRecord)
|
|
11
|
+
require_relative "solid_observer/queue_event_buffer" if defined?(ActiveRecord)
|
|
12
|
+
require_relative "solid_observer/subscriber" if defined?(ActiveSupport)
|
|
13
|
+
require_relative "solid_observer/cli/base"
|
|
14
|
+
require_relative "solid_observer/cli/status"
|
|
15
|
+
require_relative "solid_observer/cli/storage"
|
|
16
|
+
require_relative "solid_observer/cli/jobs"
|
|
17
|
+
require_relative "solid_observer/queue_stats"
|
|
18
|
+
require_relative "solid_observer/engine" if defined?(Rails::Engine)
|
|
19
|
+
|
|
20
|
+
module SolidObserver
|
|
21
|
+
class Error < StandardError; end
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
def configuration
|
|
25
|
+
@configuration ||= Configuration.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def configure
|
|
29
|
+
yield(configuration)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def config
|
|
33
|
+
configuration
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def reset_configuration!
|
|
37
|
+
@configuration = Configuration.new
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :solid_observer do
|
|
4
|
+
desc "Display SolidObserver version"
|
|
5
|
+
task :version do
|
|
6
|
+
puts "SolidObserver #{SolidObserver::VERSION}"
|
|
7
|
+
puts "Ruby: #{SolidObserver::RUBY_MINIMUM_VERSION}+"
|
|
8
|
+
puts "Rails: #{SolidObserver::RAILS_MINIMUM_VERSION}+"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
namespace :install do
|
|
12
|
+
desc "Copy SolidObserver migrations to your application"
|
|
13
|
+
task migrations: :environment do
|
|
14
|
+
Rake::Task["railties:install:migrations"].reenable
|
|
15
|
+
Rake::Task["railties:install:migrations"].invoke
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
namespace :db do
|
|
20
|
+
desc "Create the SolidObserver database"
|
|
21
|
+
task create: :environment do
|
|
22
|
+
config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, name: "solid_observer_queue")
|
|
23
|
+
ActiveRecord::Tasks::DatabaseTasks.create(config)
|
|
24
|
+
puts "Created database '#{config.database}'"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
desc "Migrate the SolidObserver database"
|
|
28
|
+
task migrate: :environment do
|
|
29
|
+
ActiveRecord::Tasks::DatabaseTasks.migrate
|
|
30
|
+
puts "Migrations complete"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
namespace :buffer do
|
|
35
|
+
desc "Flush the event buffer to the database"
|
|
36
|
+
task flush: :environment do
|
|
37
|
+
buffer = SolidObserver::QueueEventBuffer.instance
|
|
38
|
+
buffer_size = buffer.size
|
|
39
|
+
|
|
40
|
+
if buffer_size.zero?
|
|
41
|
+
puts "Buffer is empty, nothing to flush"
|
|
42
|
+
else
|
|
43
|
+
puts "Flushing #{buffer_size} events from buffer..."
|
|
44
|
+
buffer.flush!
|
|
45
|
+
puts "✓ Buffer flushed successfully"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
desc "Clear the event buffer without flushing to database"
|
|
50
|
+
task clear: :environment do
|
|
51
|
+
buffer = SolidObserver::QueueEventBuffer.instance
|
|
52
|
+
buffer_size = buffer.size
|
|
53
|
+
|
|
54
|
+
if buffer_size.zero?
|
|
55
|
+
puts "Buffer is already empty"
|
|
56
|
+
else
|
|
57
|
+
buffer.clear
|
|
58
|
+
puts "✓ Cleared #{buffer_size} events from buffer (not saved to database)"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
namespace :storage do
|
|
64
|
+
desc "Run storage cleanup based on retention policy"
|
|
65
|
+
task cleanup: :environment do
|
|
66
|
+
retention = SolidObserver.config.event_retention
|
|
67
|
+
puts "Running storage cleanup (retention: #{retention.inspect})..."
|
|
68
|
+
|
|
69
|
+
deleted_count = SolidObserver::Services::CleanupStorage.call
|
|
70
|
+
puts "✓ Cleanup complete: #{deleted_count} old events deleted"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
desc "Purge ALL SolidObserver storage data (use with caution!)"
|
|
74
|
+
task purge: :environment do
|
|
75
|
+
print "⚠️ This will delete ALL SolidObserver data (events + snapshots). Are you sure? (y/N) "
|
|
76
|
+
$stdout.flush
|
|
77
|
+
|
|
78
|
+
confirmation = $stdin.gets&.strip&.downcase
|
|
79
|
+
if confirmation == "y"
|
|
80
|
+
event_count = SolidObserver::QueueEvent.count
|
|
81
|
+
snapshot_count = SolidObserver::StorageInfo.count
|
|
82
|
+
|
|
83
|
+
SolidObserver::QueueEvent.delete_all
|
|
84
|
+
SolidObserver::StorageInfo.delete_all
|
|
85
|
+
|
|
86
|
+
connection = SolidObserver::QueueEvent.connection
|
|
87
|
+
case connection.adapter_name.downcase
|
|
88
|
+
when "sqlite"
|
|
89
|
+
connection.execute("VACUUM")
|
|
90
|
+
puts "✓ Database vacuumed to reclaim disk space"
|
|
91
|
+
when "postgresql"
|
|
92
|
+
connection.execute("ANALYZE solid_observer_queue_events")
|
|
93
|
+
connection.execute("ANALYZE solid_observer_storage_info")
|
|
94
|
+
puts "✓ Database tables analyzed"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
puts "✓ Purged #{event_count} events and #{snapshot_count} storage snapshots"
|
|
98
|
+
else
|
|
99
|
+
puts "Aborted"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
desc "Display SolidObserver status with queue statistics"
|
|
105
|
+
task status: :environment do
|
|
106
|
+
SolidObserver::CLI::Status.call
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
desc "Display storage information and database statistics"
|
|
110
|
+
task storage: :environment do
|
|
111
|
+
SolidObserver::CLI::Storage.call
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
namespace :jobs do
|
|
115
|
+
desc "List jobs with optional filters (status, queue, class, limit)"
|
|
116
|
+
task :list, [:status, :queue, :job_class, :limit] => :environment do |_t, args|
|
|
117
|
+
SolidObserver::CLI::Jobs.new.list(
|
|
118
|
+
status: args[:status],
|
|
119
|
+
queue: args[:queue],
|
|
120
|
+
job_class: args[:job_class],
|
|
121
|
+
limit: args[:limit] || 20
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
desc "Show details for a specific job by ID"
|
|
126
|
+
task :show, [:job_id] => :environment do |_t, args|
|
|
127
|
+
if args[:job_id].nil?
|
|
128
|
+
puts "Error: Job ID required. Usage: rails solid_observer:jobs:show[JOB_ID]"
|
|
129
|
+
exit 1
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
SolidObserver::CLI::Jobs.new.show(args[:job_id])
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
desc "Retry a failed job by ID"
|
|
136
|
+
task :retry, [:job_id] => :environment do |_t, args|
|
|
137
|
+
if args[:job_id].nil?
|
|
138
|
+
puts "Error: Job ID required. Usage: rails solid_observer:jobs:retry[JOB_ID]"
|
|
139
|
+
exit 1
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
SolidObserver::CLI::Jobs.new.retry_job(args[:job_id])
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
desc "Discard a failed job by ID"
|
|
146
|
+
task :discard, [:job_id] => :environment do |_t, args|
|
|
147
|
+
if args[:job_id].nil?
|
|
148
|
+
puts "Error: Job ID required. Usage: rails solid_observer:jobs:discard[JOB_ID]"
|
|
149
|
+
exit 1
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
SolidObserver::CLI::Jobs.new.discard(args[:job_id])
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: solid_observer
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- BartOz
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '8.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '8.0'
|
|
26
|
+
description: Production-grade observability for Rails 8's Solid Stack. Monitor ActiveJob
|
|
27
|
+
performance, track queue metrics, and debug issues with zero external dependencies.
|
|
28
|
+
Built-in CLI, retention policies, and APM integrations.
|
|
29
|
+
email:
|
|
30
|
+
- bartek.ozdoba@gmail.com
|
|
31
|
+
executables: []
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- CHANGELOG.md
|
|
36
|
+
- LICENSE.txt
|
|
37
|
+
- README.md
|
|
38
|
+
- app/jobs/solid_observer/cleanup_job.rb
|
|
39
|
+
- app/models/solid_observer/queue_event.rb
|
|
40
|
+
- app/models/solid_observer/queue_metric.rb
|
|
41
|
+
- app/models/solid_observer/storage_info.rb
|
|
42
|
+
- bin/console
|
|
43
|
+
- bin/setup
|
|
44
|
+
- db/migrate/20260115000001_create_solid_observer_queue_events.rb
|
|
45
|
+
- db/migrate/20260115000002_create_solid_observer_metrics.rb
|
|
46
|
+
- db/migrate/20260115000003_create_solid_observer_storage_info.rb
|
|
47
|
+
- lib/generators/solid_observer/install_generator.rb
|
|
48
|
+
- lib/generators/solid_observer/templates/initializer.rb.tt
|
|
49
|
+
- lib/solid_observer.rb
|
|
50
|
+
- lib/solid_observer/base_event.rb
|
|
51
|
+
- lib/solid_observer/base_metric.rb
|
|
52
|
+
- lib/solid_observer/cli/base.rb
|
|
53
|
+
- lib/solid_observer/cli/jobs.rb
|
|
54
|
+
- lib/solid_observer/cli/status.rb
|
|
55
|
+
- lib/solid_observer/cli/storage.rb
|
|
56
|
+
- lib/solid_observer/configuration.rb
|
|
57
|
+
- lib/solid_observer/correlation_id_resolver.rb
|
|
58
|
+
- lib/solid_observer/engine.rb
|
|
59
|
+
- lib/solid_observer/queue_event_buffer.rb
|
|
60
|
+
- lib/solid_observer/queue_stats.rb
|
|
61
|
+
- lib/solid_observer/services/cleanup_storage.rb
|
|
62
|
+
- lib/solid_observer/services/flush_event_buffer.rb
|
|
63
|
+
- lib/solid_observer/services/record_event.rb
|
|
64
|
+
- lib/solid_observer/subscriber.rb
|
|
65
|
+
- lib/solid_observer/version.rb
|
|
66
|
+
- lib/tasks/solid_observer.rake
|
|
67
|
+
homepage: https://solid.observer
|
|
68
|
+
licenses:
|
|
69
|
+
- MIT
|
|
70
|
+
metadata:
|
|
71
|
+
allowed_push_host: https://rubygems.org
|
|
72
|
+
homepage_uri: https://solid.observer
|
|
73
|
+
source_code_uri: https://github.com/bart-oz/solid_observer
|
|
74
|
+
changelog_uri: https://github.com/bart-oz/solid_observer/blob/main/CHANGELOG.md
|
|
75
|
+
rubygems_mfa_required: 'true'
|
|
76
|
+
rdoc_options: []
|
|
77
|
+
require_paths:
|
|
78
|
+
- lib
|
|
79
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
80
|
+
requirements:
|
|
81
|
+
- - ">="
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: 3.2.0
|
|
84
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0'
|
|
89
|
+
requirements: []
|
|
90
|
+
rubygems_version: 4.0.3
|
|
91
|
+
specification_version: 4
|
|
92
|
+
summary: Observability for the Rails 8's Solid Stack
|
|
93
|
+
test_files: []
|