solid_ops 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.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.DS_Store +0 -0
  3. data/CHANGELOG.md +5 -0
  4. data/CODE_OF_CONDUCT.md +10 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +308 -0
  7. data/Rakefile +12 -0
  8. data/app/assets/stylesheets/solid_ops/application.css +1 -0
  9. data/app/controllers/solid_ops/application_controller.rb +127 -0
  10. data/app/controllers/solid_ops/cache_entries_controller.rb +38 -0
  11. data/app/controllers/solid_ops/channels_controller.rb +30 -0
  12. data/app/controllers/solid_ops/dashboard_controller.rb +80 -0
  13. data/app/controllers/solid_ops/events_controller.rb +37 -0
  14. data/app/controllers/solid_ops/jobs_controller.rb +64 -0
  15. data/app/controllers/solid_ops/processes_controller.rb +11 -0
  16. data/app/controllers/solid_ops/queues_controller.rb +75 -0
  17. data/app/controllers/solid_ops/recurring_tasks_controller.rb +11 -0
  18. data/app/helpers/solid_ops/application_helper.rb +112 -0
  19. data/app/jobs/solid_ops/purge_job.rb +16 -0
  20. data/app/models/solid_ops/event.rb +34 -0
  21. data/app/views/layouts/solid_ops/application.html.erb +118 -0
  22. data/app/views/solid_ops/cache_entries/index.html.erb +86 -0
  23. data/app/views/solid_ops/cache_entries/show.html.erb +153 -0
  24. data/app/views/solid_ops/channels/index.html.erb +81 -0
  25. data/app/views/solid_ops/channels/show.html.erb +66 -0
  26. data/app/views/solid_ops/dashboard/cable.html.erb +98 -0
  27. data/app/views/solid_ops/dashboard/cache.html.erb +104 -0
  28. data/app/views/solid_ops/dashboard/index.html.erb +169 -0
  29. data/app/views/solid_ops/dashboard/jobs.html.erb +108 -0
  30. data/app/views/solid_ops/events/index.html.erb +98 -0
  31. data/app/views/solid_ops/events/show.html.erb +108 -0
  32. data/app/views/solid_ops/jobs/failed.html.erb +89 -0
  33. data/app/views/solid_ops/jobs/running.html.erb +134 -0
  34. data/app/views/solid_ops/jobs/show.html.erb +116 -0
  35. data/app/views/solid_ops/processes/index.html.erb +69 -0
  36. data/app/views/solid_ops/queues/index.html.erb +182 -0
  37. data/app/views/solid_ops/queues/show.html.erb +121 -0
  38. data/app/views/solid_ops/recurring_tasks/index.html.erb +64 -0
  39. data/app/views/solid_ops/shared/_nav.html.erb +50 -0
  40. data/app/views/solid_ops/shared/_pagination.html.erb +31 -0
  41. data/app/views/solid_ops/shared/_time_window.html.erb +10 -0
  42. data/app/views/solid_ops/shared/component_unavailable.html.erb +63 -0
  43. data/config/routes.rb +49 -0
  44. data/db/migrate/20260224000100_create_solid_ops_events.rb +31 -0
  45. data/lib/generators/solid_ops/install/install_generator.rb +348 -0
  46. data/lib/generators/solid_ops/install/templates/create_solid_ops_events.rb +31 -0
  47. data/lib/generators/solid_ops/install/templates/solid_ops_initializer.rb +31 -0
  48. data/lib/solid_ops/configuration.rb +28 -0
  49. data/lib/solid_ops/context.rb +34 -0
  50. data/lib/solid_ops/current.rb +10 -0
  51. data/lib/solid_ops/engine.rb +60 -0
  52. data/lib/solid_ops/job_extension.rb +50 -0
  53. data/lib/solid_ops/middleware.rb +52 -0
  54. data/lib/solid_ops/subscribers.rb +215 -0
  55. data/lib/solid_ops/version.rb +5 -0
  56. data/lib/solid_ops.rb +25 -0
  57. data/lib/tasks/solid_ops.rake +32 -0
  58. data/log/test.log +2 -0
  59. data/sig/solid_ops.rbs +4 -0
  60. metadata +119 -0
@@ -0,0 +1,215 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidOps
4
+ module Subscribers
5
+ class << self
6
+ def install!
7
+ return unless SolidOps.configuration.enabled
8
+
9
+ subscribe_cable!
10
+ subscribe_jobs!
11
+ subscribe_cache!
12
+ end
13
+
14
+ private
15
+
16
+ def subscribe_cable!
17
+ ActiveSupport::Notifications.subscribe("broadcast.action_cable") do |*args|
18
+ e = ActiveSupport::Notifications::Event.new(*args)
19
+ p = e.payload || {}
20
+
21
+ record_event!(
22
+ event_type: "cable.broadcast",
23
+ name: (p[:broadcasting] || p[:stream] || p[:channel] || "unknown").to_s,
24
+ duration_ms: e.duration,
25
+ metadata: {
26
+ broadcasting: p[:broadcasting],
27
+ message_bytes: bytesize(p[:message])
28
+ }
29
+ )
30
+ end
31
+ end
32
+
33
+ def subscribe_jobs!
34
+ ActiveSupport::Notifications.subscribe("enqueue.active_job") do |*args|
35
+ e = ActiveSupport::Notifications::Event.new(*args)
36
+ p = e.payload || {}
37
+ job = p[:job]
38
+
39
+ record_event!(
40
+ event_type: "job.enqueue",
41
+ name: job_name(job),
42
+ duration_ms: e.duration,
43
+ metadata: job_metadata(job).merge(queue: p[:queue].to_s, adapter: p[:adapter].to_s)
44
+ )
45
+ end
46
+
47
+ ActiveSupport::Notifications.subscribe("perform_start.active_job") do |*args|
48
+ e = ActiveSupport::Notifications::Event.new(*args)
49
+ p = e.payload || {}
50
+ job = p[:job]
51
+
52
+ record_event!(
53
+ event_type: "job.perform_start",
54
+ name: job_name(job),
55
+ duration_ms: e.duration,
56
+ metadata: job_metadata(job)
57
+ )
58
+ end
59
+
60
+ ActiveSupport::Notifications.subscribe("perform.active_job") do |*args|
61
+ e = ActiveSupport::Notifications::Event.new(*args)
62
+ p = e.payload || {}
63
+ job = p[:job]
64
+
65
+ record_event!(
66
+ event_type: "job.perform",
67
+ name: job_name(job),
68
+ duration_ms: e.duration,
69
+ metadata: job_metadata(job).merge(exception: p[:exception_object]&.class&.name)
70
+ )
71
+ end
72
+ end
73
+
74
+ def subscribe_cache!
75
+ ActiveSupport::Notifications.subscribe("cache_read.active_support") do |*args|
76
+ e = ActiveSupport::Notifications::Event.new(*args)
77
+ p = e.payload || {}
78
+
79
+ record_event!(
80
+ event_type: "cache.read",
81
+ name: p[:key].to_s,
82
+ duration_ms: e.duration,
83
+ metadata: {
84
+ hit: p[:hit],
85
+ store: p[:store].to_s
86
+ }
87
+ )
88
+ end
89
+
90
+ ActiveSupport::Notifications.subscribe("cache_write.active_support") do |*args|
91
+ e = ActiveSupport::Notifications::Event.new(*args)
92
+ p = e.payload || {}
93
+
94
+ record_event!(
95
+ event_type: "cache.write",
96
+ name: p[:key].to_s,
97
+ duration_ms: e.duration,
98
+ metadata: {
99
+ store: p[:store].to_s,
100
+ value_bytes: bytesize(p[:value])
101
+ }
102
+ )
103
+ end
104
+
105
+ ActiveSupport::Notifications.subscribe("cache_delete.active_support") do |*args|
106
+ e = ActiveSupport::Notifications::Event.new(*args)
107
+ p = e.payload || {}
108
+
109
+ record_event!(
110
+ event_type: "cache.delete",
111
+ name: p[:key].to_s,
112
+ duration_ms: e.duration,
113
+ metadata: {
114
+ store: p[:store].to_s
115
+ }
116
+ )
117
+ end
118
+ end
119
+
120
+ def record_event!(event_type:, name:, duration_ms:, metadata:)
121
+ return if Thread.current[:solid_ops_recording]
122
+ return unless SolidOps.configuration.sample?
123
+
124
+ Thread.current[:solid_ops_recording] = true
125
+
126
+ SolidOps::Context.ensure_correlation_id!
127
+
128
+ meta = metadata || {}
129
+ meta = SolidOps.configuration.redactor.call(meta) if SolidOps.configuration.redactor
130
+ meta = truncate_meta(meta)
131
+
132
+ SolidOps::Event.create!(
133
+ event_type: event_type,
134
+ name: name.to_s,
135
+ correlation_id: SolidOps::Current.correlation_id,
136
+ request_id: SolidOps::Current.request_id,
137
+ tenant_id: SolidOps::Current.tenant_id,
138
+ actor_id: SolidOps::Current.actor_id,
139
+ duration_ms: duration_ms,
140
+ occurred_at: Time.current,
141
+ metadata: meta
142
+ )
143
+ rescue StandardError => e
144
+ Rails.logger.warn("[SolidOps] Failed to record event: #{e.class}: #{e.message}") if defined?(Rails.logger)
145
+ nil
146
+ ensure
147
+ Thread.current[:solid_ops_recording] = false
148
+ end
149
+
150
+ def truncate_meta(meta)
151
+ max = SolidOps.configuration.max_payload_bytes.to_i
152
+ safe = safe_serialize(meta)
153
+ return safe if max <= 0
154
+
155
+ json = safe.to_json
156
+ return safe if json.bytesize <= max
157
+
158
+ { truncated: true, max_bytes: max, bytes: json.bytesize }
159
+ rescue StandardError
160
+ { unserializable: true }
161
+ end
162
+
163
+ def job_name(job)
164
+ return "unknown" unless job
165
+
166
+ job.class.name.to_s
167
+ end
168
+
169
+ def job_metadata(job)
170
+ return {} unless job
171
+
172
+ {
173
+ job_id: job.job_id,
174
+ provider_job_id: job.provider_job_id,
175
+ queue_name: job.queue_name,
176
+ arguments: safe_arguments(job.arguments)
177
+ }
178
+ end
179
+
180
+ def safe_arguments(args)
181
+ max = SolidOps.configuration.max_payload_bytes.to_i
182
+ json = Array(args).map { |a| safe_serialize(a) }.to_json
183
+ return Array(args).map { |a| safe_serialize(a) } if max <= 0 || json.bytesize <= max
184
+
185
+ { truncated: true, max_bytes: max }
186
+ rescue StandardError
187
+ { unserializable: true }
188
+ end
189
+
190
+ def safe_serialize(obj)
191
+ case obj
192
+ when String, Numeric, NilClass, TrueClass, FalseClass
193
+ obj
194
+ when Hash
195
+ obj.transform_values { |v| safe_serialize(v) }
196
+ when Array
197
+ obj.map { |v| safe_serialize(v) }
198
+ else
199
+ obj.to_s
200
+ end
201
+ rescue StandardError
202
+ obj.class.name.to_s
203
+ end
204
+
205
+ def bytesize(value)
206
+ return nil if value.nil?
207
+
208
+ s = value.is_a?(String) ? value : value.to_s
209
+ s.bytesize
210
+ rescue StandardError
211
+ nil
212
+ end
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidOps
4
+ VERSION = "0.1.0"
5
+ end
data/lib/solid_ops.rb ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+ require_relative "solid_ops/version"
5
+ require_relative "solid_ops/configuration"
6
+ require_relative "solid_ops/current"
7
+ require_relative "solid_ops/context"
8
+ require_relative "solid_ops/middleware"
9
+ require_relative "solid_ops/job_extension"
10
+ require_relative "solid_ops/subscribers"
11
+ require_relative "solid_ops/engine"
12
+
13
+ module SolidOps
14
+ class Error < StandardError; end
15
+
16
+ class << self
17
+ def configuration
18
+ @configuration ||= SolidOps::Configuration.new
19
+ end
20
+
21
+ def configure
22
+ yield(configuration)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :solid_ops do
4
+ desc "Purge events older than the configured retention period"
5
+ task purge: :environment do
6
+ retention = SolidOps.configuration.retention_period
7
+ unless retention
8
+ puts "No retention_period configured. Set SolidOps.configure { |c| c.retention_period = 7.days }"
9
+ next
10
+ end
11
+
12
+ cutoff = retention.ago
13
+ deleted = SolidOps::Event.purge!(before: cutoff)
14
+ puts "SolidOps: Purged #{deleted} events older than #{cutoff}"
15
+ end
16
+
17
+ desc "Show event count and storage stats"
18
+ task stats: :environment do
19
+ total = SolidOps::Event.count
20
+ oldest = SolidOps::Event.minimum(:occurred_at)
21
+ newest = SolidOps::Event.maximum(:occurred_at)
22
+
23
+ puts "SolidOps Event Stats"
24
+ puts " Total events: #{total}"
25
+ puts " Oldest: #{oldest || "none"}"
26
+ puts " Newest: #{newest || "none"}"
27
+ puts ""
28
+ SolidOps::Event.group(:event_type).count.sort_by { |_, v| -v }.each do |type, count|
29
+ puts " #{type}: #{count}"
30
+ end
31
+ end
32
+ end
data/log/test.log ADDED
@@ -0,0 +1,2 @@
1
+ [SolidOps] WARNING: No auth_check configured — the dashboard is publicly accessible. Set SolidOps.configure { |c| c.auth_check = ->(controller) { controller.current_user&.admin? } } in an initializer to restrict access.
2
+ [SolidOps] WARNING: No auth_check configured — the dashboard is publicly accessible. Set SolidOps.configure { |c| c.auth_check = ->(controller) { controller.current_user&.admin? } } in an initializer to restrict access.
data/sig/solid_ops.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module SolidOps
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: solid_ops
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - samuel-murphy
8
+ bindir: exe
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: '7.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.1'
26
+ description: SolidOps provides a real-time dashboard and management UI for Solid Queue,
27
+ Solid Cache, and Solid Cable — built as a mountable Rails engine with zero JavaScript
28
+ dependencies.
29
+ email:
30
+ - samuelmurphy15@gmail.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - ".DS_Store"
36
+ - CHANGELOG.md
37
+ - CODE_OF_CONDUCT.md
38
+ - LICENSE.txt
39
+ - README.md
40
+ - Rakefile
41
+ - app/assets/stylesheets/solid_ops/application.css
42
+ - app/controllers/solid_ops/application_controller.rb
43
+ - app/controllers/solid_ops/cache_entries_controller.rb
44
+ - app/controllers/solid_ops/channels_controller.rb
45
+ - app/controllers/solid_ops/dashboard_controller.rb
46
+ - app/controllers/solid_ops/events_controller.rb
47
+ - app/controllers/solid_ops/jobs_controller.rb
48
+ - app/controllers/solid_ops/processes_controller.rb
49
+ - app/controllers/solid_ops/queues_controller.rb
50
+ - app/controllers/solid_ops/recurring_tasks_controller.rb
51
+ - app/helpers/solid_ops/application_helper.rb
52
+ - app/jobs/solid_ops/purge_job.rb
53
+ - app/models/solid_ops/event.rb
54
+ - app/views/layouts/solid_ops/application.html.erb
55
+ - app/views/solid_ops/cache_entries/index.html.erb
56
+ - app/views/solid_ops/cache_entries/show.html.erb
57
+ - app/views/solid_ops/channels/index.html.erb
58
+ - app/views/solid_ops/channels/show.html.erb
59
+ - app/views/solid_ops/dashboard/cable.html.erb
60
+ - app/views/solid_ops/dashboard/cache.html.erb
61
+ - app/views/solid_ops/dashboard/index.html.erb
62
+ - app/views/solid_ops/dashboard/jobs.html.erb
63
+ - app/views/solid_ops/events/index.html.erb
64
+ - app/views/solid_ops/events/show.html.erb
65
+ - app/views/solid_ops/jobs/failed.html.erb
66
+ - app/views/solid_ops/jobs/running.html.erb
67
+ - app/views/solid_ops/jobs/show.html.erb
68
+ - app/views/solid_ops/processes/index.html.erb
69
+ - app/views/solid_ops/queues/index.html.erb
70
+ - app/views/solid_ops/queues/show.html.erb
71
+ - app/views/solid_ops/recurring_tasks/index.html.erb
72
+ - app/views/solid_ops/shared/_nav.html.erb
73
+ - app/views/solid_ops/shared/_pagination.html.erb
74
+ - app/views/solid_ops/shared/_time_window.html.erb
75
+ - app/views/solid_ops/shared/component_unavailable.html.erb
76
+ - config/routes.rb
77
+ - db/migrate/20260224000100_create_solid_ops_events.rb
78
+ - lib/generators/solid_ops/install/install_generator.rb
79
+ - lib/generators/solid_ops/install/templates/create_solid_ops_events.rb
80
+ - lib/generators/solid_ops/install/templates/solid_ops_initializer.rb
81
+ - lib/solid_ops.rb
82
+ - lib/solid_ops/configuration.rb
83
+ - lib/solid_ops/context.rb
84
+ - lib/solid_ops/current.rb
85
+ - lib/solid_ops/engine.rb
86
+ - lib/solid_ops/job_extension.rb
87
+ - lib/solid_ops/middleware.rb
88
+ - lib/solid_ops/subscribers.rb
89
+ - lib/solid_ops/version.rb
90
+ - lib/tasks/solid_ops.rake
91
+ - log/test.log
92
+ - sig/solid_ops.rbs
93
+ homepage: https://github.com/h0m1c1de/solid_ops
94
+ licenses:
95
+ - MIT
96
+ metadata:
97
+ allowed_push_host: https://rubygems.org
98
+ homepage_uri: https://github.com/h0m1c1de/solid_ops
99
+ source_code_uri: https://github.com/h0m1c1de/solid_ops
100
+ changelog_uri: https://github.com/h0m1c1de/solid_ops/blob/main/CHANGELOG.md
101
+ rubygems_mfa_required: 'true'
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: 3.2.0
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ requirements: []
116
+ rubygems_version: 4.0.3
117
+ specification_version: 4
118
+ summary: Rails-native observability and control plane for the Solid Trifecta
119
+ test_files: []