solid_observer 0.3.0 → 0.5.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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/README.md +195 -82
  4. data/app/assets/javascripts/solid_observer/live_poll.js +3 -1
  5. data/app/controllers/solid_observer/application_controller.rb +1 -0
  6. data/app/controllers/solid_observer/cable_dashboard_controller.rb +52 -0
  7. data/app/controllers/solid_observer/cable_operations_controller.rb +16 -0
  8. data/app/controllers/solid_observer/cache_dashboard_controller.rb +52 -0
  9. data/app/controllers/solid_observer/cache_operations_controller.rb +24 -0
  10. data/app/controllers/solid_observer/dashboard_controller.rb +38 -1
  11. data/app/controllers/solid_observer/storages_controller.rb +1 -1
  12. data/app/helpers/solid_observer/application_helper.rb +268 -5
  13. data/app/helpers/solid_observer/dashboard_helper.rb +30 -11
  14. data/app/models/solid_observer/cable_event.rb +13 -0
  15. data/app/models/solid_observer/cable_metric.rb +12 -0
  16. data/app/models/solid_observer/cache_event.rb +15 -0
  17. data/app/models/solid_observer/cache_metric.rb +13 -0
  18. data/app/models/solid_observer/storage_info.rb +4 -1
  19. data/app/views/layouts/solid_observer/application.html.erb +157 -19
  20. data/app/views/solid_observer/cable_dashboard/_charts.html.erb +31 -0
  21. data/app/views/solid_observer/cable_dashboard/_recent_events.html.erb +34 -0
  22. data/app/views/solid_observer/cable_dashboard/_summary.html.erb +34 -0
  23. data/app/views/solid_observer/cable_dashboard/index.html.erb +118 -0
  24. data/app/views/solid_observer/cache_dashboard/_charts.html.erb +40 -0
  25. data/app/views/solid_observer/cache_dashboard/_recent_events.html.erb +34 -0
  26. data/app/views/solid_observer/cache_dashboard/_summary.html.erb +39 -0
  27. data/app/views/solid_observer/cache_dashboard/index.html.erb +62 -0
  28. data/app/views/solid_observer/cache_operations/_confirm_clear.html.erb +6 -0
  29. data/app/views/solid_observer/cache_operations/index.html.erb +60 -0
  30. data/app/views/solid_observer/dashboard/_queue_table.html.erb +1 -0
  31. data/app/views/solid_observer/dashboard/index.html.erb +32 -5
  32. data/app/views/solid_observer/events/index.html.erb +1 -0
  33. data/app/views/solid_observer/jobs/index.html.erb +1 -0
  34. data/app/views/solid_observer/jobs/show.html.erb +3 -3
  35. data/app/views/solid_observer/storages/show.html.erb +90 -32
  36. data/config/routes.rb +7 -0
  37. data/db/migrate/20260601000001_create_solid_observer_cache_events.rb +22 -0
  38. data/db/migrate/20260601000002_create_solid_observer_cache_metrics.rb +18 -0
  39. data/db/migrate/20260602000001_add_component_to_solid_observer_storage_infos.rb +8 -0
  40. data/db/migrate/20260612000001_add_event_type_recorded_at_index_to_cache_events.rb +21 -0
  41. data/db/migrate/20260619000001_create_solid_observer_cable_events.rb +22 -0
  42. data/db/migrate/20260619000002_create_solid_observer_cable_metrics.rb +17 -0
  43. data/lib/generators/solid_observer/install_generator.rb +8 -1
  44. data/lib/generators/solid_observer/templates/initializer.rb.tt +20 -4
  45. data/lib/solid_observer/base_event.rb +1 -1
  46. data/lib/solid_observer/base_metric.rb +1 -1
  47. data/lib/solid_observer/base_record.rb +8 -0
  48. data/lib/solid_observer/cable_event_buffer.rb +28 -0
  49. data/lib/solid_observer/cable_metric_buffer.rb +230 -0
  50. data/lib/solid_observer/cable_subscriber.rb +57 -0
  51. data/lib/solid_observer/cache_event_buffer.rb +28 -0
  52. data/lib/solid_observer/cache_metric_buffer.rb +229 -0
  53. data/lib/solid_observer/cache_subscriber.rb +47 -0
  54. data/lib/solid_observer/chart_buffer.rb +84 -27
  55. data/lib/solid_observer/cli/storage.rb +16 -13
  56. data/lib/solid_observer/configuration.rb +67 -5
  57. data/lib/solid_observer/engine.rb +70 -15
  58. data/lib/solid_observer/event_buffer_core.rb +218 -0
  59. data/lib/solid_observer/queue_event_buffer.rb +9 -201
  60. data/lib/solid_observer/services/cable_operations.rb +74 -0
  61. data/lib/solid_observer/services/cable_stats.rb +385 -0
  62. data/lib/solid_observer/services/cache_operations.rb +115 -0
  63. data/lib/solid_observer/services/cache_stats.rb +346 -0
  64. data/lib/solid_observer/services/cleanup_storage.rb +98 -47
  65. data/lib/solid_observer/services/database_size.rb +13 -8
  66. data/lib/solid_observer/services/flush_cable_event_buffer.rb +54 -0
  67. data/lib/solid_observer/services/flush_cable_metrics.rb +54 -0
  68. data/lib/solid_observer/services/flush_cache_event_buffer.rb +54 -0
  69. data/lib/solid_observer/services/flush_cache_metrics.rb +56 -0
  70. data/lib/solid_observer/services/record_cable_event.rb +114 -0
  71. data/lib/solid_observer/services/record_cable_metric.rb +73 -0
  72. data/lib/solid_observer/services/record_cache_event.rb +165 -0
  73. data/lib/solid_observer/services/record_cache_metric.rb +66 -0
  74. data/lib/solid_observer/services/storage_info_snapshot.rb +216 -0
  75. data/lib/solid_observer/version.rb +1 -1
  76. data/lib/solid_observer.rb +36 -11
  77. data/lib/tasks/solid_observer.rake +111 -21
  78. metadata +47 -5
  79. data/bin/console +0 -11
  80. data/bin/quality_gate +0 -95
  81. data/bin/setup +0 -8
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dashboard_controller"
4
+
5
+ module SolidObserver
6
+ class CacheDashboardController < DashboardController
7
+ CACHE_STORAGE_COMPONENTS = %w[solid_cache cache_observer].freeze
8
+
9
+ def index
10
+ @component = "cache"
11
+ assign_cache_dashboard
12
+ end
13
+
14
+ private
15
+
16
+ def assign_cache_dashboard
17
+ unless SolidObserver.config.solid_cache_enabled?
18
+ @cache_dashboard_available = false
19
+ @storage_components = []
20
+ @recent_events = []
21
+ @activity_trends = SolidObserver::Services::CacheStats::ACTIVITY_TREND_EMPTY
22
+ @stability = SolidObserver::Services::CacheStats::STABILITY_EMPTY
23
+ return
24
+ end
25
+
26
+ range = SolidObserver::Services::CacheStats.parse_range(request_range_param)
27
+ window = SolidObserver::Services::CacheStats.range_duration(range)
28
+ stats = SolidObserver::Services::CacheStats.call(window: window)
29
+
30
+ @cache_dashboard_available = true
31
+ @range = range
32
+ @stats = stats
33
+ @activity_trends = stats[:activity_trends]
34
+ @stability = stats[:stability]
35
+ @storage_components = cache_storage_components
36
+ @recent_events = recent_events(window)
37
+ end
38
+
39
+ def cache_storage_components
40
+ SolidObserver::Services::StorageInfoSnapshot.call.select do |snapshot|
41
+ CACHE_STORAGE_COMPONENTS.include?(snapshot[:component])
42
+ end
43
+ end
44
+
45
+ def recent_events(window)
46
+ current_time = Time.current
47
+ SolidObserver::CacheEvent.where(recorded_at: (current_time - window)..current_time).recent(10)
48
+ rescue ActiveRecord::StatementInvalid
49
+ []
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidObserver
4
+ class CacheOperationsController < ApplicationController
5
+ def index
6
+ @cache_controls_available = SolidObserver::Services::CacheOperations.available?
7
+ end
8
+
9
+ def prune
10
+ redirect_with_result(SolidObserver::Services::CacheOperations.prune)
11
+ end
12
+
13
+ def clear
14
+ redirect_with_result(SolidObserver::Services::CacheOperations.clear)
15
+ end
16
+
17
+ private
18
+
19
+ def redirect_with_result(result)
20
+ flash_key = result[:ok] ? :notice : :alert
21
+ redirect_to cache_operations_path, flash_key => result[:message]
22
+ end
23
+ end
24
+ end
@@ -1,16 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../../helpers/solid_observer/application_helper"
4
+
3
5
  module SolidObserver
4
6
  class DashboardController < ApplicationController
7
+ helper SolidObserver::ApplicationHelper
8
+
5
9
  skip_forgery_protection only: :live_poll
6
10
  skip_after_action :verify_same_origin_request, only: :live_poll
7
11
 
8
12
  def index
13
+ @component = selected_component
14
+
15
+ return unless @component == "queue" && SolidObserver.config.solid_queue_enabled?
16
+
9
17
  assign_range_and_stats
10
18
  load_persistence_data if persistence_mode?
11
19
  end
12
20
 
13
21
  def live_poll
22
+ expires_in 1.day, public: true
14
23
  send_file(
15
24
  SolidObserver::Engine.root.join("app/assets/javascripts/solid_observer/live_poll.js"),
16
25
  type: "application/javascript; charset=utf-8",
@@ -32,7 +41,13 @@ module SolidObserver
32
41
  @range = range
33
42
  @live = request_live_param == "on"
34
43
  @stats = QueueStats.snapshot(range: range)
35
- @chart = QueueStats.chart_data(window: QueueStats.range_duration(@range))
44
+ @chart = if @stats[:available]
45
+ QueueStats.chart_data(window: QueueStats.range_duration(@range))
46
+ else
47
+ {performed: [], failed: [], ready: []}
48
+ end
49
+ rescue
50
+ @chart = {performed: [], failed: [], ready: []}
36
51
  end
37
52
 
38
53
  def load_persistence_data
@@ -75,5 +90,27 @@ module SolidObserver
75
90
  def append_chart_buffer
76
91
  ChartBuffer.append(SolidQueue::ReadyExecution.count) if QueueStats.solid_queue_available?
77
92
  end
93
+
94
+ def selected_component
95
+ requested = if request&.respond_to?(:path_parameters)
96
+ request.path_parameters&.[](:component).to_s
97
+ else
98
+ ""
99
+ end
100
+ requested = path_component if requested.empty?
101
+ return "cache" if requested == "cache" && SolidObserver.config.solid_cache_enabled?
102
+
103
+ "queue"
104
+ end
105
+
106
+ def path_component
107
+ return "" unless request&.respond_to?(:path)
108
+
109
+ path = request&.path.to_s
110
+ return "cache" if path.end_with?("/cache")
111
+ return "queue" if path.end_with?("/queue")
112
+
113
+ ""
114
+ end
78
115
  end
79
116
  end
@@ -5,7 +5,7 @@ module SolidObserver
5
5
  include RequirePersistenceMode
6
6
 
7
7
  def show
8
- @current_storage = SolidObserver::StorageInfo.order(recorded_at: :desc).first
8
+ @storage_components = SolidObserver::Services::StorageInfoSnapshot.call
9
9
  @storage_history = SolidObserver::StorageInfo.recent(20)
10
10
  end
11
11
  end
@@ -1,7 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "dashboard_helper"
4
+
3
5
  module SolidObserver
4
6
  module ApplicationHelper
7
+ include SolidObserver::DashboardHelper
8
+
5
9
  STATUS_COLORS = {
6
10
  "completed" => "success",
7
11
  "ready" => "success",
@@ -23,6 +27,21 @@ module SolidObserver
23
27
  degraded: {label: "Degraded", tone: "warning"},
24
28
  critical: {label: "Critical", tone: "danger"}
25
29
  }.freeze
30
+ CACHE_OUTCOME_STATES = {
31
+ hit: {label: "Hit", tone: "success"},
32
+ miss: {label: "Miss", tone: "info"},
33
+ error: {label: "Error", tone: "danger"},
34
+ recorded: {label: "Recorded", tone: "recorded"}
35
+ }.freeze
36
+ CACHE_RANGE_LABELS = {
37
+ "15m" => "in last 15m",
38
+ "30m" => "in last 30m",
39
+ "1h" => "in last hour",
40
+ "7h" => "in last 7h",
41
+ "1d" => "in last day",
42
+ "7d" => "in last 7d",
43
+ "14d" => "in last 14d"
44
+ }.freeze
26
45
 
27
46
  def execution_status(execution)
28
47
  ExecutionPresenter.new(execution).status
@@ -41,7 +60,15 @@ module SolidObserver
41
60
  def status_badge(status)
42
61
  status_str = status.to_s
43
62
  color = STATUS_COLORS.fetch(status_str, "default")
44
- content_tag(:span, status_str.humanize, class: "so-badge so-badge--#{color}")
63
+ dot = tag.svg(
64
+ tag.circle(r: 3, cx: 3, cy: 3),
65
+ class: "so-badge__dot",
66
+ viewBox: "0 0 6 6",
67
+ "aria-hidden": "true"
68
+ )
69
+ tag.span(class: "so-badge so-badge--pill so-badge--#{color}") do
70
+ safe_join([dot, status_str.humanize], " ")
71
+ end
45
72
  end
46
73
 
47
74
  def duration_with_semantic(value, event_type)
@@ -53,7 +80,15 @@ module SolidObserver
53
80
  def mode_badge
54
81
  config = SolidObserver.config
55
82
  color = config.persistence_mode? ? "info" : "warning"
56
- content_tag(:span, config.storage_mode.to_s.capitalize, class: "so-badge so-badge--#{color}")
83
+ dot = tag.svg(
84
+ tag.circle(r: 3, cx: 3, cy: 3),
85
+ class: "so-badge__dot",
86
+ viewBox: "0 0 6 6",
87
+ "aria-hidden": "true"
88
+ )
89
+ tag.span(class: "so-badge so-badge--pill so-badge--#{color}") do
90
+ safe_join([dot, config.storage_mode.to_s.capitalize], " ")
91
+ end
57
92
  end
58
93
 
59
94
  def turbo_frame_tag(id, **options, &block)
@@ -73,14 +108,125 @@ module SolidObserver
73
108
  end
74
109
 
75
110
  def stability_badge(stats)
76
- meta = STABILITY_STATES.fetch(stability_state(stats))
77
- dot = tag.svg(tag.circle(r: 3, cx: 3, cy: 3),
78
- class: "so-badge__dot", viewBox: "0 0 6 6", "aria-hidden": "true")
111
+ stability_badge_for(stability_state(stats))
112
+ end
113
+
114
+ def cache_stability_badge(state)
115
+ stability_badge_for(state.to_sym)
116
+ end
117
+
118
+ def cache_ratio_percent(value)
119
+ number_to_percentage(value.to_f * 100, precision: 1, strip_insignificant_zeros: true)
120
+ end
121
+
122
+ def cache_storage_summary(storage_components)
123
+ snapshots = Array(storage_components)
124
+ reason = cache_storage_unavailable_reason(snapshots)
125
+ return {value: "—", subtitle: "— #{reason}"} if reason
126
+
127
+ {
128
+ value: number_to_human_size(cache_storage_total_bytes(snapshots), precision: 1, significant: false, strip_insignificant_zeros: false),
129
+ subtitle: "SolidCache + cache observer"
130
+ }
131
+ end
132
+
133
+ def cache_event_outcome_badge(event)
134
+ meta = cache_event_outcome_meta(event)
135
+ dot = tag.svg(
136
+ tag.circle(r: 3, cx: 3, cy: 3),
137
+ class: "so-badge__dot",
138
+ viewBox: "0 0 6 6",
139
+ "aria-hidden": "true"
140
+ )
141
+
79
142
  tag.span(class: "so-badge so-badge--pill so-badge--#{meta[:tone]}") do
80
143
  safe_join([dot, meta[:label]], " ")
81
144
  end
82
145
  end
83
146
 
147
+ def cache_event_digest(key_digest, visible_chars: 10)
148
+ digest = key_digest.to_s
149
+ return "—" if digest.empty?
150
+ return digest if digest.length <= visible_chars
151
+
152
+ "#{digest.first(visible_chars)}…"
153
+ end
154
+
155
+ def cache_range_label(range_key)
156
+ CACHE_RANGE_LABELS.fetch(range_key.to_s, "in selected range")
157
+ end
158
+
159
+ def cable_range_label(range_key)
160
+ CACHE_RANGE_LABELS.fetch(range_key.to_s, "in selected range")
161
+ end
162
+
163
+ def cable_ratio_percent(value)
164
+ number_to_percentage(value.to_f * 100, precision: 1, strip_insignificant_zeros: true)
165
+ end
166
+
167
+ def cable_stability_badge(state)
168
+ stability_badge_for(state.to_sym)
169
+ end
170
+
171
+ def cable_stability_detail(stability)
172
+ state = (stability || {})[:state]&.to_sym
173
+ state = :stable unless STABILITY_STATES.key?(state)
174
+
175
+ case state
176
+ when :critical
177
+ critical_cable_stability_detail(stability)
178
+ when :degraded
179
+ degraded_cable_stability_detail(stability)
180
+ else
181
+ "No cable errors or subscription rejections in the selected range and backlog current snapshot is healthy"
182
+ end
183
+ end
184
+
185
+ def cable_event_digest(digest, visible_chars: 10)
186
+ digest = digest.to_s
187
+ return "—" if digest.empty?
188
+ return digest if digest.length <= visible_chars
189
+
190
+ "#{digest.first(visible_chars)}…"
191
+ end
192
+
193
+ # :reek:FeatureEnvy
194
+ def cable_backlog_summary(stats)
195
+ if stats[:backlog_available]
196
+ {
197
+ value: number_with_delimiter(stats[:backlog_count].to_i),
198
+ subtitle: "current Solid Cable snapshot"
199
+ }
200
+ else
201
+ {value: "—", subtitle: "current Solid Cable snapshot unavailable"}
202
+ end
203
+ end
204
+
205
+ def cable_storage_summary(storage_components)
206
+ snapshots = Array(storage_components)
207
+ reason = cable_storage_unavailable_reason(snapshots)
208
+ return {value: "—", subtitle: "— #{reason}"} if reason
209
+
210
+ {
211
+ value: number_to_human_size(cable_storage_total_bytes(snapshots), precision: 1, significant: false, strip_insignificant_zeros: false),
212
+ subtitle: "Cable telemetry + Solid Cable messages"
213
+ }
214
+ end
215
+
216
+ def cache_stability_detail(stability)
217
+ state = (stability || {})[:state]&.to_sym
218
+ state = :stable unless STABILITY_STATES.key?(state)
219
+
220
+ case state
221
+ when :critical
222
+ critical_cache_stability_detail(stability)
223
+ when :degraded
224
+ degraded_cache_stability_detail(stability)
225
+ else
226
+ "No sampled cache errors or slow events in the selected range"
227
+ end
228
+ end
229
+
84
230
  def stability_detail(stats)
85
231
  failures_24h = stats[:failed_last_24h].to_i
86
232
  return "No failures in the last 24h" if failures_24h.zero?
@@ -91,5 +237,122 @@ module SolidObserver
91
237
  def latest_failure_phrase(timestamp)
92
238
  timestamp ? "#{time_ago_in_words(timestamp)} ago" : "unknown"
93
239
  end
240
+
241
+ def queue_component_enabled?
242
+ SolidObserver.config.solid_queue_enabled?
243
+ end
244
+
245
+ def cache_component_enabled?
246
+ SolidObserver.config.solid_cache_enabled?
247
+ end
248
+
249
+ def cable_component_enabled?
250
+ SolidObserver.config.solid_cable_enabled?
251
+ end
252
+
253
+ def dashboard_section_active?(component)
254
+ current_component = @component.presence || "queue"
255
+ controller_name == "dashboard" && current_component == component.to_s
256
+ end
257
+
258
+ private
259
+
260
+ def stability_badge_for(state)
261
+ meta = STABILITY_STATES.fetch(state)
262
+ dot = tag.svg(tag.circle(r: 3, cx: 3, cy: 3),
263
+ class: "so-badge__dot", viewBox: "0 0 6 6", "aria-hidden": "true")
264
+ tag.span(class: "so-badge so-badge--pill so-badge--#{meta[:tone]}") do
265
+ safe_join([dot, meta[:label]], " ")
266
+ end
267
+ end
268
+
269
+ def critical_cache_stability_detail(stability)
270
+ detail = pluralize(stability[:error_count].to_i, "sampled cache error")
271
+ slow_count = stability[:slow_count].to_i
272
+ detail = "#{detail} and #{pluralize(slow_count, "slow event")}" if slow_count.positive?
273
+ "#{detail} in the selected range#{cache_stability_latest_suffix(stability[:latest_recorded_at])}"
274
+ end
275
+
276
+ def degraded_cache_stability_detail(stability)
277
+ detail = pluralize(stability[:slow_count].to_i, "slow sampled cache event")
278
+ "#{detail} in the selected range#{cache_stability_latest_suffix(stability[:latest_recorded_at])}"
279
+ end
280
+
281
+ def cache_stability_latest_suffix(timestamp)
282
+ timestamp ? ", latest #{time_ago_in_words(timestamp)} ago" : ""
283
+ end
284
+
285
+ def cache_storage_total_bytes(snapshots)
286
+ snapshots.sum { |snapshot| snapshot[:db_size_bytes].to_i }
287
+ end
288
+
289
+ def cache_storage_unavailable_reason(snapshots)
290
+ return "Storage snapshot unavailable" unless snapshots.size == 2
291
+
292
+ snapshots.find { |snapshot| !snapshot[:available] }&.[](:unavailable_reason)
293
+ end
294
+
295
+ # :reek:FeatureEnvy
296
+ # :reek:TooManyStatements
297
+ def critical_cable_stability_detail(stability)
298
+ parts = []
299
+ error_count = stability[:error_count].to_i
300
+ parts << pluralize(error_count, "cable error") if error_count.positive?
301
+
302
+ rejection_count = stability[:rejection_count].to_i
303
+ rejection_rate = stability[:rejection_rate].to_f
304
+ if rejection_rate > 0.0
305
+ parts << "#{cable_ratio_percent(rejection_rate)} rejection rate"
306
+ elsif rejection_count.positive?
307
+ parts << pluralize(rejection_count, "subscription rejection")
308
+ end
309
+
310
+ backlog_ratio = stability[:backlog_ratio].to_f
311
+ if backlog_ratio >= 0.5
312
+ parts << "backlog at #{number_to_percentage(backlog_ratio * 100, precision: 0)} in current snapshot"
313
+ end
314
+
315
+ return "Cable stability critical" if parts.empty?
316
+
317
+ "#{parts.join("; ")} in the selected range"
318
+ end
319
+
320
+ # :reek:FeatureEnvy
321
+ # :reek:TooManyStatements
322
+ def degraded_cable_stability_detail(stability)
323
+ return "Backlog current snapshot unavailable" unless stability[:backlog_available]
324
+
325
+ backlog_ratio = stability[:backlog_ratio].to_f
326
+ if backlog_ratio >= SolidObserver.config.cable_backlog_threshold.to_f
327
+ return "Backlog at #{number_to_percentage(backlog_ratio * 100, precision: 0)} in current snapshot"
328
+ end
329
+
330
+ rejection_count = stability[:rejection_count].to_i
331
+ if rejection_count.positive?
332
+ return "#{pluralize(rejection_count, "subscription rejection")} in the selected range"
333
+ end
334
+
335
+ "Cable stability degraded"
336
+ end
337
+
338
+ def cable_storage_total_bytes(snapshots)
339
+ snapshots.sum { |snapshot| snapshot[:db_size_bytes].to_i }
340
+ end
341
+
342
+ def cable_storage_unavailable_reason(snapshots)
343
+ return "Storage snapshot unavailable" unless snapshots.size == 2
344
+
345
+ snapshots.find { |snapshot| !snapshot[:available] }&.[](:unavailable_reason)
346
+ end
347
+
348
+ def cache_event_outcome_meta(event)
349
+ hit = event.hit
350
+
351
+ return CACHE_OUTCOME_STATES.fetch(:error) if event.error_class.present?
352
+ return CACHE_OUTCOME_STATES.fetch(:hit) if hit == true
353
+ return CACHE_OUTCOME_STATES.fetch(:miss) if hit == false
354
+
355
+ CACHE_OUTCOME_STATES.fetch(:recorded)
356
+ end
94
357
  end
95
358
  end
@@ -9,17 +9,36 @@ module SolidObserver
9
9
  def spark_points(series, width: SVG_W, height: SVG_H)
10
10
  return "" if series.blank?
11
11
 
12
- t_min = series.first[:t]
13
- t_max = series.last[:t]
14
- v_max = [series.max_by { |point| point[:v] }[:v], 1].max
15
- time_span = t_max - t_min
16
-
17
- series.map { |point|
18
- val = point[:v]
19
- point_x = time_span.zero? ? width / 2.0 : ((point[:t] - t_min).to_f / time_span) * (width - 2) + 1
20
- point_y = height - 1 - (val.to_f / v_max) * (height - 2)
21
- format("%.1f,%.1f", point_x, point_y)
22
- }.join(" ")
12
+ context = build_spark_context(series, width, height)
13
+ series.map { |point| format_spark_point(point: point, context: context) }.join(" ")
14
+ end
15
+
16
+ def build_spark_context(series, width, height)
17
+ first_point = series.first
18
+ last_point = series.last
19
+ min_time = first_point[:t]
20
+
21
+ {
22
+ min_time: min_time,
23
+ time_span: last_point[:t] - min_time,
24
+ max_value: [series.max_by { |point| point[:v] }[:v], 1].max,
25
+ width: width,
26
+ height: height,
27
+ inner_width: width - 2,
28
+ inner_height: height - 2
29
+ }
30
+ end
31
+
32
+ def format_spark_point(point:, context:)
33
+ time_span = context[:time_span]
34
+ coordinate_x = if time_span.zero?
35
+ context[:width] / 2.0
36
+ else
37
+ ((point[:t] - context[:min_time]).to_f / time_span) * context[:inner_width] + 1
38
+ end
39
+
40
+ coordinate_y = context[:height] - 1 - (point[:v].to_f / context[:max_value]) * context[:inner_height]
41
+ format("%.1f,%.1f", coordinate_x, coordinate_y)
23
42
  end
24
43
 
25
44
  RANGE_LABELS = {
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidObserver
4
+ class CableEvent < BaseEvent
5
+ self.table_name = "solid_observer_cable_events"
6
+
7
+ validates :event_type, presence: true
8
+ validates :recorded_at, presence: true
9
+
10
+ scope :errored, -> { where.not(error_class: nil) }
11
+ scope :recent, ->(limit = 10) { order(recorded_at: :desc).limit(limit) }
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidObserver
4
+ class CableMetric < BaseRecord
5
+ self.table_name = "solid_observer_cable_metrics"
6
+
7
+ validates :period_start, presence: true
8
+ validates :broadcasts_count, :transmissions_count, :confirmations_count,
9
+ :rejections_count, :perform_actions_count, :errors_count,
10
+ numericality: {only_integer: true, greater_than_or_equal_to: 0}
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidObserver
4
+ class CacheEvent < BaseEvent
5
+ self.table_name = "solid_observer_cache_events"
6
+
7
+ validates :event_type, presence: true
8
+ validates :key_digest, presence: true
9
+ validates :recorded_at, presence: true
10
+
11
+ scope :errored, -> { where.not(error_class: nil) }
12
+ scope :slow, ->(threshold = SolidObserver.config.cache_slow_threshold) { where("duration >= ?", threshold) }
13
+ scope :recent, ->(limit = 10) { order(recorded_at: :desc).limit(limit) }
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidObserver
4
+ class CacheMetric < BaseRecord
5
+ self.table_name = "solid_observer_cache_metrics"
6
+
7
+ validates :event_type, presence: true, length: {maximum: 64}
8
+ validates :period_start, presence: true
9
+ validates :operations_count, :hits_count, :misses_count, :errors_count,
10
+ numericality: {only_integer: true, greater_than_or_equal_to: 0}
11
+ validates :duration_total, numericality: {greater_than_or_equal_to: 0}
12
+ end
13
+ end
@@ -6,16 +6,19 @@ module SolidObserver
6
6
 
7
7
  MB_TO_BYTES = 1_048_576
8
8
  GB_TO_BYTES = 1_073_741_824
9
+ COMPONENTS = %w[queue_observer cache_observer solid_cache cable_observer solid_cable].freeze
9
10
 
10
11
  validates :db_size_bytes, presence: true, numericality: {only_integer: true, greater_than_or_equal_to: 0}
11
12
  validates :event_count, presence: true, numericality: {only_integer: true, greater_than_or_equal_to: 0}
12
13
  validates :recorded_at, presence: true
14
+ validates :component, presence: true, inclusion: {in: COMPONENTS}
13
15
 
14
16
  scope :recent, ->(limit = 10) { order(recorded_at: :desc).limit(limit) }
15
17
  scope :since, ->(time) { where("recorded_at >= ?", time) }
16
18
 
17
- def self.record_snapshot(db_size:, event_count:)
19
+ def self.record_snapshot(db_size:, event_count:, component: "queue_observer")
18
20
  create!(
21
+ component: component,
19
22
  db_size_bytes: db_size || 0,
20
23
  event_count: event_count,
21
24
  recorded_at: Time.current