rails_nexus 2.0.0 → 2.0.1

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.
@@ -1,128 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RailsNexus
4
- class DatabaseStat < BaseRecord
5
- self.table_name = "rails_nexus_database_stats"
6
-
7
- validates :recorded_at, presence: true
8
-
9
- scope :recent, ->(hours = 24) { where("recorded_at >= ?", hours.hours.ago) }
10
- scope :with_tables, -> { where.not(table_name: nil) }
11
- scope :pool_stats, -> { where(table_name: nil) }
12
-
13
- # Collect database health snapshot
14
- def self.collect!
15
- now = Time.current
16
- adapter = ActiveRecord::Base.connection.adapter_name.downcase
17
-
18
- # Connection pool stats
19
- pool = ActiveRecord::Base.connection_pool
20
- stats = pool_stats(pool)
21
- create!(stats.merge(recorded_at: now))
22
-
23
- # Table stats (MySQL only)
24
- if adapter.include?("mysql")
25
- collect_mysql_stats(now)
26
- elsif adapter.include?("postgresql")
27
- collect_postgresql_stats(now)
28
- end
29
- end
30
-
31
- # Get pool utilization over time
32
- def self.pool_utilization(hours: 24)
33
- pool_stats
34
- .where("recorded_at >= ?", hours.hours.ago)
35
- .order(:recorded_at)
36
- .map { |s| { time: s.recorded_at, utilization: s.utilization, busy: s.busy, idle: s.idle } }
37
- end
38
-
39
- # Get slow queries
40
- def self.slow_queries(limit: 10)
41
- where.not(slow_queries: nil)
42
- .order(recorded_at: :desc)
43
- .limit(1)
44
- .flat_map(&:slow_queries)
45
- .first(limit)
46
- end
47
-
48
- # Get N+1 patterns
49
- def self.n1_patterns
50
- where.not(n1_patterns: nil)
51
- .order(recorded_at: :desc)
52
- .limit(1)
53
- .flat_map(&:n1_patterns)
54
- end
55
-
56
- private
57
-
58
- def self.pool_stats(pool)
59
- {
60
- pool_size: pool.size,
61
- busy: pool.connections.size,
62
- idle: pool.size - pool.connections.size,
63
- dead: 0,
64
- waiting: pool.stat[:waiting] || 0,
65
- utilization: pool.size > 0 ? (pool.connections.size.to_f / pool.size * 100).round(1) : 0
66
- }
67
- end
68
-
69
- def self.collect_mysql_stats(now)
70
- conn = ActiveRecord::Base.connection
71
- tables = conn.execute(<<~SQL)
72
- SELECT table_name, table_rows, data_length, index_length, data_free
73
- FROM information_schema.tables
74
- WHERE table_schema = DATABASE()
75
- ORDER BY data_length + index_length DESC
76
- LIMIT 50
77
- SQL
78
-
79
- tables.each do |row|
80
- create!(
81
- table_name: row["TABLE_NAME"],
82
- table_rows: row["TABLE_ROWS"],
83
- table_size: format_size(row["DATA_LENGTH"]),
84
- index_size: format_size(row["INDEX_LENGTH"]),
85
- data_free: format_size(row["DATA_FREE"]),
86
- recorded_at: now
87
- )
88
- end
89
- rescue StandardError => e
90
- Rails.logger.error("[RailsNexus] Failed to collect MySQL stats: #{e.message}") if defined?(Rails)
91
- end
92
-
93
- def self.collect_postgresql_stats(now)
94
- conn = ActiveRecord::Base.connection
95
- tables = conn.execute(<<~SQL)
96
- SELECT schemaname, relname, n_tup_ins, n_tup_upd, n_tup_del,
97
- pg_size_pretty(pg_total_relation_size(relid)) as total_size,
98
- pg_size_pretty(pg_indexes_size(relid)) as index_size
99
- FROM pg_stat_user_tables
100
- ORDER BY pg_total_relation_size(relid) DESC
101
- LIMIT 50
102
- SQL
103
-
104
- tables.each do |row|
105
- create!(
106
- table_name: row["relname"],
107
- table_rows: (row["n_tup_ins"].to_i + row["n_tup_upd"].to_i - row["n_tup_del"].to_i),
108
- table_size: row["total_size"],
109
- index_size: row["index_size"],
110
- recorded_at: now
111
- )
112
- end
113
- rescue StandardError => e
114
- Rails.logger.error("[RailsNexus] Failed to collect PostgreSQL stats: #{e.message}") if defined?(Rails)
115
- end
116
-
117
- def self.format_size(bytes)
118
- return "0 B" unless bytes
119
- units = ["B", "KB", "MB", "GB", "TB"]
120
- size = bytes.to_f
121
- units.each do |unit|
122
- return "#{size.round(1)} #{unit}" if size < 1024
123
- size /= 1024
124
- end
125
- "#{size.round(1)} PB"
126
- end
127
- end
128
- end
@@ -1,101 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RailsNexus
4
- class Event < BaseRecord
5
- self.table_name = "rails_nexus_events"
6
-
7
- belongs_to :eventable, polymorphic: true, optional: true
8
-
9
- validates :event_type, presence: true, inclusion: { in: %w[
10
- breadcrumb storm_start storm_end
11
- assignment priority_change snooze unsnooze
12
- mute unmute comment note status_change
13
- backup_start backup_complete backup_failed
14
- exception_created exception_resolved
15
- ] }
16
-
17
- validates :created_at, presence: true
18
-
19
- scope :recent, ->(hours = 24) { where("created_at >= ?", hours.hours.ago) }
20
- scope :by_type, ->(type) { where(event_type: type) }
21
- scope :breadcrumbs, -> { by_type("breadcrumb") }
22
- scope :workflow_events, -> { where(event_type: %w[assignment priority_change snooze unsnooze mute unmute comment note]) }
23
- scope :storm_events, -> { where(event_type: %w[storm_start storm_end]) }
24
- scope :audit_trail, ->(eventable) { where(eventable: eventable).order(created_at: :desc) }
25
-
26
- # Record a breadcrumb
27
- def self.breadcrumb!(message:, metadata: {}, author: nil)
28
- create!(
29
- event_type: "breadcrumb",
30
- message: message,
31
- metadata: metadata,
32
- author: author,
33
- created_at: Time.current
34
- )
35
- end
36
-
37
- # Record a storm event
38
- def self.storm_start!(threshold:, current_rate:)
39
- create!(
40
- event_type: "storm_start",
41
- message: "Error storm detected: #{current_rate} errors/sec (threshold: #{threshold}/sec)",
42
- metadata: { threshold: threshold, current_rate: current_rate },
43
- created_at: Time.current
44
- )
45
- end
46
-
47
- def self.storm_end!(duration_seconds:)
48
- create!(
49
- event_type: "storm_end",
50
- message: "Error storm ended after #{duration_seconds.round(1)}s",
51
- metadata: { duration_seconds: duration_seconds },
52
- created_at: Time.current
53
- )
54
- end
55
-
56
- # Record a workflow event
57
- def self.workflow!(event_type:, eventable:, message:, author: nil, metadata: {})
58
- create!(
59
- event_type: event_type,
60
- eventable: eventable,
61
- message: message,
62
- author: author,
63
- metadata: metadata,
64
- created_at: Time.current
65
- )
66
- end
67
-
68
- # Get event timeline for an exception
69
- def self.timeline_for(exception)
70
- where(eventable: exception).order(created_at: :asc)
71
- end
72
-
73
- # Cleanup old events
74
- def self.cleanup!(retention_days: 30)
75
- where("created_at < ?", retention_days.days.ago).delete_all
76
- end
77
-
78
- # Human-readable event type
79
- def type_label
80
- case event_type
81
- when "breadcrumb" then "🔍 Breadcrumb"
82
- when "storm_start" then "🌪️ Storm Start"
83
- when "storm_end" then "☀️ Storm End"
84
- when "assignment" then "👤 Assigned"
85
- when "priority_change" then " priority Changed"
86
- when "snooze" then "😴 Snoozed"
87
- when "unsnooze" then "⏰ Unsnoozed"
88
- when "mute" then "🔇 Muted"
89
- when "unmute" then "🔊 Unmuted"
90
- when "comment" then "💬 Comment"
91
- when "note" then "📝 Note"
92
- when "backup_start" then "💾 Backup Started"
93
- when "backup_complete" then "✅ Backup Complete"
94
- when "backup_failed" then "❌ Backup Failed"
95
- when "exception_created" then "🚨 Exception Created"
96
- when "exception_resolved" then "✓ Exception Resolved"
97
- else event_type.humanize
98
- end
99
- end
100
- end
101
- end
@@ -1,107 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RailsNexus
4
- class Metric < BaseRecord
5
- self.table_name = "rails_nexus_metrics"
6
-
7
- validates :metric_type, presence: true
8
- validates :value, presence: true
9
- validates :recorded_at, presence: true
10
-
11
- scope :recent, ->(hours = 24) { where("recorded_at >= ?", hours.hours.ago) }
12
- scope :by_type, ->(type) { where(metric_type: type) }
13
- scope :cpu, -> { by_type("cpu") }
14
- scope :memory, -> { by_type("memory") }
15
- scope :threads, -> { by_type("threads") }
16
- scope :db_pool, -> { by_type("db_pool") }
17
- scope :disk, -> { by_type("disk") }
18
-
19
- # Collect current system metrics
20
- def self.collect!
21
- now = Time.current
22
-
23
- # CPU usage
24
- cpu_usage = detect_cpu_usage
25
- create!(metric_type: "cpu", value: cpu_usage, unit: "%", recorded_at: now) if cpu_usage
26
-
27
- # Memory
28
- mem = detect_memory_usage
29
- if mem
30
- create!(metric_type: "memory_total", value: mem[:total], unit: "mb", recorded_at: now)
31
- create!(metric_type: "memory_used", value: mem[:used], unit: "mb", recorded_at: now)
32
- create!(metric_type: "memory_free", value: mem[:free], unit: "mb", recorded_at: now)
33
- end
34
-
35
- # Threads
36
- create!(metric_type: "threads", value: Thread.list.size, unit: "count", recorded_at: now)
37
-
38
- # DB pool
39
- db_pool = detect_db_pool
40
- if db_pool
41
- create!(metric_type: "db_pool_size", value: db_pool[:size], unit: "count", recorded_at: now)
42
- create!(metric_type: "db_pool_busy", value: db_pool[:busy], unit: "count", recorded_at: now)
43
- create!(metric_type: "db_pool_idle", value: db_pool[:idle], unit: "count", recorded_at: now)
44
- end
45
-
46
- # GC stats
47
- gc = GC.stat
48
- create!(metric_type: "gc_count", value: gc[:count].to_f, unit: "count", recorded_at: now)
49
- create!(metric_type: "gc_heap_allocated", value: (gc[:heap_allocated_objects].to_f / 1_000_000), unit: "m", recorded_at: now)
50
- end
51
-
52
- # Get metric as a time series for charts
53
- def self.time_series(type, hours: 24, interval: 15)
54
- by_type(type)
55
- .where("recorded_at >= ?", hours.hours.ago)
56
- .order(:recorded_at)
57
- .group_by { |m| (m.recorded_at.to_i / (interval * 60)) * (interval * 60) }
58
- .map { |ts, records| { time: Time.at(ts), value: records.map(&:value).sum / records.size.to_f } }
59
- end
60
-
61
- # Cleanup old metrics
62
- def self.cleanup!(retention_hours: 168)
63
- where("recorded_at < ?", retention_hours.hours.ago).delete_all
64
- end
65
-
66
- private
67
-
68
- def self.detect_cpu_usage
69
- # Linux: read /proc/stat
70
- if File.exist?("/proc/stat")
71
- line = File.readlines("/proc/stat").first
72
- values = line.split[1..].map(&:to_f)
73
- idle = values[3]
74
- total = values.sum
75
- # Approximate — real implementation needs two readings
76
- ((total - idle) / total * 100).round(1)
77
- end
78
- rescue StandardError
79
- nil
80
- end
81
-
82
- def self.detect_memory_usage
83
- if File.exist?("/proc/meminfo")
84
- info = {}
85
- File.readlines("/proc/meminfo").each do |line|
86
- key, val = line.split(":")
87
- info[key.strip] = val.strip.split.first.to_i if key && val
88
- end
89
- total = (info["MemTotal"] || 0) / 1024
90
- free = (info["MemAvailable"] || info["MemFree"] || 0) / 1024
91
- used = total - free
92
- { total: total.round, used: used.round, free: free.round }
93
- end
94
- rescue StandardError
95
- nil
96
- end
97
-
98
- def self.detect_db_pool
99
- if ActiveRecord::Base.connection_pool
100
- pool = ActiveRecord::Base.connection_pool
101
- { size: pool.size, busy: pool.connections.size, idle: pool.size - pool.connections.size }
102
- end
103
- rescue StandardError
104
- nil
105
- end
106
- end
107
- end
@@ -1,112 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RailsNexus
4
- class NginxMetric < BaseRecord
5
- self.table_name = "rails_nexus_nginx_metrics"
6
-
7
- validates :status_code, presence: true
8
- validates :recorded_at, presence: true
9
-
10
- scope :recent, ->(hours = 24) { where("recorded_at >= ?", hours.hours.ago) }
11
- scope :by_status, ->(code) { where(status_code: code) }
12
- scope :errors, -> { where("status_code >= 400") }
13
- scope :by_path, ->(path) { where(request_path: path) }
14
-
15
- # Parse nginx access log file
16
- def self.parse_log!(log_path, limit: 1000)
17
- return unless File.exist?(log_path)
18
- return unless File.readable?(log_path)
19
-
20
- entries = []
21
- File.foreach(log_path).last(limit).each do |line|
22
- entry = parse_log_line(line)
23
- entries << entry if entry
24
- end
25
-
26
- insert_all(entries) if entries.any?
27
- entries.size
28
- end
29
-
30
- # Get status code distribution
31
- def self.status_distribution(hours: 24)
32
- recent(hours)
33
- .group(:status_code)
34
- .order("count_all DESC")
35
- .count
36
- end
37
-
38
- # Get top slow endpoints
39
- def self.slow_endpoints(hours: 24, limit: 10)
40
- recent(hours)
41
- .where.not(response_time: nil)
42
- .group(:request_path)
43
- .order("AVG(response_time) DESC")
44
- .limit(limit)
45
- .average(:response_time)
46
- .map { |path, avg_time| { path: path, avg_response_time: avg_time&.round(3) } }
47
- end
48
-
49
- # Get requests per minute over time
50
- def self.requests_over_time(hours: 24, interval_minutes: 5)
51
- recent(hours)
52
- .order(:recorded_at)
53
- .group_by { |r| (r.recorded_at.to_i / (interval_minutes * 60)) * (interval_minutes * 60) }
54
- .map do |ts, records|
55
- {
56
- time: Time.at(ts),
57
- total: records.size,
58
- errors: records.count { |r| r.status_code >= 400 },
59
- avg_response_time: records.filter_map(&:response_time).then { |t| t.any? ? (t.sum / t.size).round(3) : nil }
60
- }
61
- end
62
- end
63
-
64
- # Get error rate
65
- def self.error_rate(hours: 24)
66
- total = recent(hours).count
67
- return 0 if total.zero?
68
- errors = recent(hours).errors.count
69
- (errors.to_f / total * 100).round(2)
70
- end
71
-
72
- # Cleanup old metrics
73
- def self.cleanup!(retention_days: 7)
74
- where("recorded_at < ?", retention_days.days.ago).delete_all
75
- end
76
-
77
- private
78
-
79
- # Parse a single nginx log line (combined format)
80
- # Format: $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" $request_time $upstream_response_time
81
- def self.parse_log_line(line)
82
- pattern = /^(\S+) \S+ (\S+) \[([^\]]+)\] "(\S+) (\S+)(?:\s+\S+)?" (\d{3}) (\d+|-) "([^"]*)" "([^"]*)" ?(\d+\.?\d*)? ?(\d+\.?\d*)?$/
83
-
84
- match = line.match(pattern)
85
- return unless match
86
-
87
- status_code = match[6].to_i
88
- response_time = match[10]&.to_f
89
- upstream_time = match[11]&.to_f
90
-
91
- {
92
- status_code: status_code,
93
- request_method: match[4],
94
- request_path: match[5],
95
- response_time: response_time,
96
- upstream_time: upstream_time,
97
- remote_addr: match[1],
98
- user_agent: match[9],
99
- referer: match[8] != "-" ? match[8] : nil,
100
- recorded_at: parse_nginx_time(match[3])
101
- }
102
- rescue StandardError
103
- nil
104
- end
105
-
106
- def self.parse_nginx_time(time_str)
107
- Time.parse(time_str)
108
- rescue StandardError
109
- Time.current
110
- end
111
- end
112
- end
@@ -1,168 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RailsNexus
4
- class ServerMetric < BaseRecord
5
- self.table_name = "rails_nexus_server_metrics"
6
-
7
- validates :recorded_at, presence: true
8
-
9
- scope :recent, ->(hours = 24) { where("recorded_at >= ?", hours.hours.ago) }
10
- scope :latest, -> { order(recorded_at: :desc).first }
11
-
12
- # Collect current server metrics
13
- def self.collect!
14
- now = Time.current
15
- data = {
16
- hostname: Socket.gethostname,
17
- ruby_version: RUBY_VERSION,
18
- rails_version: defined?(Rails) ? Rails::VERSION::STRING : nil,
19
- os_info: detect_os,
20
- recorded_at: now
21
- }
22
-
23
- # CPU
24
- cpu = detect_cpu
25
- data.merge!(cpu) if cpu
26
-
27
- # Memory
28
- mem = detect_memory
29
- data.merge!(mem) if mem
30
-
31
- # Load averages
32
- load_avg = detect_load_average
33
- data.merge!(load_avg) if load_avg
34
-
35
- # Disk
36
- disk = detect_disk
37
- data.merge!(disk) if disk
38
-
39
- # Processes
40
- procs = detect_processes
41
- data.merge!(procs) if procs
42
-
43
- # Uptime
44
- data[:uptime_seconds] = detect_uptime
45
-
46
- create!(data)
47
- end
48
-
49
- # Get latest metrics
50
- def self.current
51
- latest || collect!
52
- end
53
-
54
- # Get metric trend over time
55
- def self.trend(field, hours: 24)
56
- where("recorded_at >= ?", hours.hours.ago)
57
- .order(:recorded_at)
58
- .pluck(:recorded_at, field.to_sym)
59
- .map { |time, value| { time: time, value: value&.to_f } }
60
- end
61
-
62
- # Cleanup old metrics
63
- def self.cleanup!(retention_days: 7)
64
- where("recorded_at < ?", retention_days.days.ago).delete_all
65
- end
66
-
67
- private
68
-
69
- def self.detect_os
70
- if File.exist?("/etc/os-release")
71
- info = {}
72
- File.readlines("/etc/os-release").each do |line|
73
- key, val = line.split("=", 2)
74
- info[key.strip] = val&.strip&.gsub('"', '') if key && val
75
- end
76
- "#{info['PRETTY_NAME'] || info['NAME']} #{info['VERSION']}"
77
- elsif RUBY_PLATFORM =~ /darwin/
78
- `sw_vers -productVersion`.strip
79
- else
80
- RUBY_PLATFORM
81
- end
82
- rescue StandardError
83
- RUBY_PLATFORM
84
- end
85
-
86
- def self.detect_cpu
87
- return unless File.exist?("/proc/cpuinfo")
88
- cores = File.readlines("/proc/cpuinfo").count { |l| l =~ /^processor\s*:/ }
89
- { cpu_cores: cores }
90
- rescue StandardError
91
- nil
92
- end
93
-
94
- def self.detect_memory
95
- return unless File.exist?("/proc/meminfo")
96
- info = {}
97
- File.readlines("/proc/meminfo").each do |line|
98
- key, val = line.split(":")
99
- next unless key && val
100
- kb = val.strip.split.first.to_i
101
- case key.strip
102
- when "MemTotal" then info[:memory_total] = kb / 1024
103
- when "MemAvailable" then info[:memory_free] = kb / 1024
104
- when "MemFree" then info[:memory_free] ||= kb / 1024
105
- when "SwapTotal" then info[:swap_total] = kb / 1024
106
- when "SwapFree" then info[:swap_used] = ((info[:swap_total] || 0) - (kb / 1024))
107
- end
108
- end
109
- info[:memory_used] = (info[:memory_total] || 0) - (info[:memory_free] || 0) if info[:memory_total]
110
- info
111
- rescue StandardError
112
- nil
113
- end
114
-
115
- def self.detect_load_average
116
- if File.exist?("/proc/loadavg")
117
- parts = File.read("/proc/loadavg").split
118
- { load_avg_1m: parts[0].to_f, load_avg_5m: parts[1].to_f, load_avg_15m: parts[2].to_f }
119
- end
120
- rescue StandardError
121
- nil
122
- end
123
-
124
- def self.detect_disk
125
- output = `df -B1 / 2>/dev/null`.lines.last
126
- return unless output
127
- parts = output.split
128
- total = parts[1].to_i
129
- used = parts[2].to_i
130
- { disk_total: total, disk_used: used, disk_usage_percent: total > 0 ? (used.to_f / total * 100).round(1) : 0 }
131
- rescue StandardError
132
- nil
133
- end
134
-
135
- def self.detect_processes
136
- data = {}
137
-
138
- # Puma
139
- if defined?(Puma) && Puma.respond_to?(:stats)
140
- stats = Puma.stats_hash rescue {}
141
- data[:puma_workers] = stats[:workers]&.size || 0
142
- data[:puma_threads] = stats[:workers]&.sum { |w| w[:last_status]&.dig(:max_threads) || 0 } || 0
143
- end
144
-
145
- # Sidekiq
146
- if defined?(Sidekiq)
147
- require "sidekiq/api"
148
- stats = Sidekiq::Stats.new
149
- data[:sidekiq_processed] = stats.processed
150
- data[:sidekiq_failed] = stats.failed
151
- data[:sidekiq_enqueued] = stats.enqueued
152
- data[:sidekiq_workers] = Sidekiq::ProcessSet.new.size rescue 0
153
- end
154
-
155
- data
156
- rescue StandardError
157
- {}
158
- end
159
-
160
- def self.detect_uptime
161
- if File.exist?("/proc/uptime")
162
- File.read("/proc/uptime").split.first.to_f
163
- end
164
- rescue StandardError
165
- nil
166
- end
167
- end
168
- end