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.
- checksums.yaml +4 -4
- data/app/controllers/rails_nexus/backup_controller.rb +23 -56
- data/app/services/rails_nexus/backup_service.rb +119 -218
- data/app/views/layouts/rails_nexus/application.html.erb +8 -0
- data/app/views/rails_nexus/backup/files.html.erb +30 -16
- data/app/views/rails_nexus/backup/index.html.erb +82 -158
- data/config/routes.rb +1 -2
- data/lib/generators/rails_nexus/install_generator.rb +1 -13
- data/lib/generators/rails_nexus/templates/migration.rb +119 -11
- data/lib/rails_nexus/version.rb +1 -1
- data/lib/tasks/rails_nexus.rake +8 -0
- metadata +1 -14
- data/app/models/rails_nexus/database_stat.rb +0 -128
- data/app/models/rails_nexus/event.rb +0 -101
- data/app/models/rails_nexus/metric.rb +0 -107
- data/app/models/rails_nexus/nginx_metric.rb +0 -112
- data/app/models/rails_nexus/server_metric.rb +0 -168
- data/app/views/rails_nexus/backup/settings.html.erb +0 -158
- data/db/migrate/20240330122311_create_rails_nexus_logged_exceptions.rb +0 -22
- data/lib/generators/rails_nexus/templates/migration_advanced_features.rb +0 -30
- data/lib/generators/rails_nexus/templates/migration_cron_jobs.rb +0 -22
- data/lib/generators/rails_nexus/templates/migration_new_schema.rb +0 -120
- data/lib/generators/rails_nexus/templates/migration_platform_detection.rb +0 -11
- data/lib/generators/rails_nexus/templates/migration_webhook_deliveries.rb +0 -21
- data/lib/generators/rails_nexus/templates/migration_workflow.rb +0 -29
|
@@ -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
|