rails_nexus 2.0.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/MIT-LICENSE +20 -0
- data/README.md +430 -0
- data/Rakefile +6 -0
- data/app/assets/config/rails_nexus_manifest.js +2 -0
- data/app/assets/javascripts/rails_nexus/controllers.js +603 -0
- data/app/assets/stylesheets/rails_nexus/application.css +1964 -0
- data/app/controllers/rails_nexus/analytics_controller.rb +361 -0
- data/app/controllers/rails_nexus/application_controller.rb +15 -0
- data/app/controllers/rails_nexus/backup_controller.rb +91 -0
- data/app/controllers/rails_nexus/cron_jobs_controller.rb +64 -0
- data/app/controllers/rails_nexus/database_health_controller.rb +128 -0
- data/app/controllers/rails_nexus/logged_exceptions_controller.rb +227 -0
- data/app/controllers/rails_nexus/n1_patterns_controller.rb +10 -0
- data/app/controllers/rails_nexus/settings_controller.rb +100 -0
- data/app/controllers/rails_nexus/source_code_controller.rb +39 -0
- data/app/controllers/rails_nexus/stats_controller.rb +494 -0
- data/app/controllers/rails_nexus/workflow_controller.rb +96 -0
- data/app/helpers/rails_nexus/application_helper.rb +5 -0
- data/app/helpers/rails_nexus/logged_exceptions_helper.rb +254 -0
- data/app/helpers/rails_nexus/source_code_helper.rb +107 -0
- data/app/javascript/controllers/rails_nexus_controller.js +380 -0
- data/app/javascript/controllers/rails_nexus_detail_controller.js +165 -0
- data/app/javascript/controllers/rails_nexus_sidebar_controller.js +15 -0
- data/app/javascript/controllers/rails_nexus_theme_controller.js +31 -0
- data/app/javascript/rails_nexus/application.js +17 -0
- data/app/jobs/rails_nexus/application_job.rb +4 -0
- data/app/mailers/rails_nexus/application_mailer.rb +6 -0
- data/app/models/rails_nexus/application_record.rb +5 -0
- data/app/models/rails_nexus/backup.rb +107 -0
- data/app/models/rails_nexus/base_record.rb +26 -0
- data/app/models/rails_nexus/comment.rb +13 -0
- data/app/models/rails_nexus/cron_job.rb +72 -0
- data/app/models/rails_nexus/database_stat.rb +128 -0
- data/app/models/rails_nexus/event.rb +101 -0
- data/app/models/rails_nexus/logged_exception.rb +864 -0
- data/app/models/rails_nexus/logged_exception_n1.rb +87 -0
- data/app/models/rails_nexus/metric.rb +107 -0
- data/app/models/rails_nexus/nginx_metric.rb +112 -0
- data/app/models/rails_nexus/server_metric.rb +168 -0
- data/app/models/rails_nexus/webhook_delivery.rb +64 -0
- data/app/services/rails_nexus/backup_service.rb +275 -0
- data/app/views/layouts/rails_nexus/application.html.erb +185 -0
- data/app/views/rails_nexus/analytics/index.html.erb +363 -0
- data/app/views/rails_nexus/backup/files.html.erb +53 -0
- data/app/views/rails_nexus/backup/index.html.erb +211 -0
- data/app/views/rails_nexus/backup/settings.html.erb +158 -0
- data/app/views/rails_nexus/cron_jobs/index.html.erb +141 -0
- data/app/views/rails_nexus/database_health/index.html.erb +139 -0
- data/app/views/rails_nexus/logged_exceptions/_comments_list.html.erb +50 -0
- data/app/views/rails_nexus/logged_exceptions/_exceptions.html.erb +190 -0
- data/app/views/rails_nexus/logged_exceptions/_feed.html.erb +3 -0
- data/app/views/rails_nexus/logged_exceptions/_filter_group.html.erb +10 -0
- data/app/views/rails_nexus/logged_exceptions/_show.html.erb +554 -0
- data/app/views/rails_nexus/logged_exceptions/_workflow_panel.html.erb +118 -0
- data/app/views/rails_nexus/logged_exceptions/clear.turbo_stream.erb +6 -0
- data/app/views/rails_nexus/logged_exceptions/destroy.turbo_stream.erb +2 -0
- data/app/views/rails_nexus/logged_exceptions/destroy_all.turbo_stream.erb +6 -0
- data/app/views/rails_nexus/logged_exceptions/feed.rss.builder +20 -0
- data/app/views/rails_nexus/logged_exceptions/index.html.erb +323 -0
- data/app/views/rails_nexus/logged_exceptions/query.html.erb +3 -0
- data/app/views/rails_nexus/logged_exceptions/query.turbo_stream.erb +6 -0
- data/app/views/rails_nexus/logged_exceptions/show.html.erb +3 -0
- data/app/views/rails_nexus/logged_exceptions/show.turbo_stream.erb +5 -0
- data/app/views/rails_nexus/n1_patterns/index.html.erb +153 -0
- data/app/views/rails_nexus/settings/index.html.erb +140 -0
- data/app/views/rails_nexus/stats/index.html.erb +569 -0
- data/config/importmap.rb +15 -0
- data/config/initializers/date_formats.rb +5 -0
- data/config/initializers/rails_nexus.rb +18 -0
- data/config/locales/en.yml +47 -0
- data/config/routes.rb +58 -0
- data/db/migrate/20240330122311_create_rails_nexus_logged_exceptions.rb +22 -0
- data/lib/generators/rails_nexus/backup_generator.rb +70 -0
- data/lib/generators/rails_nexus/customize_generator.rb +159 -0
- data/lib/generators/rails_nexus/install_generator.rb +61 -0
- data/lib/generators/rails_nexus/templates/INSTALL.md +9 -0
- data/lib/generators/rails_nexus/templates/backup/backup_helper.sh +152 -0
- data/lib/generators/rails_nexus/templates/backup/config.rb +27 -0
- data/lib/generators/rails_nexus/templates/backup/models/daily_backup.rb +24 -0
- data/lib/generators/rails_nexus/templates/backup/models/full_backup.rb +35 -0
- data/lib/generators/rails_nexus/templates/backup/models/sync_backup.rb +43 -0
- data/lib/generators/rails_nexus/templates/backup/mysql-config/db_config.cnf +15 -0
- data/lib/generators/rails_nexus/templates/backup/schedule.rb +28 -0
- data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_controller.js +310 -0
- data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_detail_controller.js +98 -0
- data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_sidebar_controller.js +15 -0
- data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_theme_controller.js +30 -0
- data/lib/generators/rails_nexus/templates/migration.rb +24 -0
- data/lib/generators/rails_nexus/templates/migration_advanced_features.rb +30 -0
- data/lib/generators/rails_nexus/templates/migration_cron_jobs.rb +22 -0
- data/lib/generators/rails_nexus/templates/migration_new_schema.rb +120 -0
- data/lib/generators/rails_nexus/templates/migration_platform_detection.rb +11 -0
- data/lib/generators/rails_nexus/templates/migration_webhook_deliveries.rb +21 -0
- data/lib/generators/rails_nexus/templates/migration_workflow.rb +29 -0
- data/lib/generators/rails_nexus/templates/rails_nexus.rb +96 -0
- data/lib/generators/rails_nexus/templates/views/layouts/rails_nexus/application.html.erb +129 -0
- data/lib/generators/rails_nexus/templates/views/rails_nexus/logged_exceptions/_exceptions.html.erb +123 -0
- data/lib/generators/rails_nexus/templates/views/rails_nexus/logged_exceptions/_show.html.erb +297 -0
- data/lib/generators/rails_nexus/templates/views/rails_nexus/logged_exceptions/index.html.erb +113 -0
- data/lib/rails_nexus/breadcrumbs.rb +161 -0
- data/lib/rails_nexus/cleanup.rb +32 -0
- data/lib/rails_nexus/configuration.rb +319 -0
- data/lib/rails_nexus/engine.rb +135 -0
- data/lib/rails_nexus/logger.rb +188 -0
- data/lib/rails_nexus/notifications.rb +105 -0
- data/lib/rails_nexus/ransack_config.rb +4 -0
- data/lib/rails_nexus/storm_protection.rb +124 -0
- data/lib/rails_nexus/version.rb +3 -0
- data/lib/rails_nexus.rb +133 -0
- data/lib/tasks/rails_nexus.rake +230 -0
- data/lib/tasks/rails_nexus_tasks.rake +37 -0
- data/rails_nexus.gemspec +36 -0
- metadata +252 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsNexus
|
|
4
|
+
class Configuration
|
|
5
|
+
# ─── General ───────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
# Dashboard title shown in the header. Set to nil to use "RailsNexus".
|
|
8
|
+
attr_accessor :application_name
|
|
9
|
+
|
|
10
|
+
# Enable/disable the dashboard entirely (default: true).
|
|
11
|
+
attr_accessor :enabled
|
|
12
|
+
|
|
13
|
+
# Number of exceptions per page (default: 30).
|
|
14
|
+
attr_accessor :per_page
|
|
15
|
+
|
|
16
|
+
# ─── Authentication ────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
# Proc called with the controller instance before each request.
|
|
19
|
+
# Return false/nil to deny access; return true to allow.
|
|
20
|
+
# Example:
|
|
21
|
+
# config.auth_block = ->(controller) { controller.current_user&.admin? }
|
|
22
|
+
attr_accessor :auth_block
|
|
23
|
+
|
|
24
|
+
# ─── Exception Data ────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
# Proc called with the controller to attach extra data to each exception record.
|
|
27
|
+
# Example:
|
|
28
|
+
# config.exception_data = ->(controller) { { user_id: controller.current_user&.id } }
|
|
29
|
+
attr_accessor :exception_data
|
|
30
|
+
|
|
31
|
+
# ─── Notifications ─────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
# Proc called with the LoggedException record after creation.
|
|
34
|
+
# Use for Telegram, Slack, email, or custom notifications.
|
|
35
|
+
# Example:
|
|
36
|
+
# config.after_create = ->(exception) { MyNotifier.alert(exception) }
|
|
37
|
+
attr_accessor :after_create
|
|
38
|
+
|
|
39
|
+
# Webhook URLs called on each new exception (POST with JSON body).
|
|
40
|
+
# Example:
|
|
41
|
+
# config.webhooks = ["https://hooks.slack.com/services/xxx"]
|
|
42
|
+
attr_accessor :webhooks
|
|
43
|
+
|
|
44
|
+
# Webhook timeout in seconds (default: 5).
|
|
45
|
+
attr_accessor :webhook_timeout
|
|
46
|
+
|
|
47
|
+
# Custom headers sent with webhook requests.
|
|
48
|
+
# Example:
|
|
49
|
+
# config.webhook_headers = { "Authorization" => "Bearer token123" }
|
|
50
|
+
attr_accessor :webhook_headers
|
|
51
|
+
|
|
52
|
+
# ─── Logging ───────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
# Enable structured logging (default: true).
|
|
55
|
+
attr_accessor :logging_enabled
|
|
56
|
+
|
|
57
|
+
# Log level for rails_nexus logger (default: :info).
|
|
58
|
+
# Options: :debug, :info, :warn, :error, :fatal
|
|
59
|
+
attr_accessor :log_level
|
|
60
|
+
|
|
61
|
+
# Write logs to a JSON file (path or nil).
|
|
62
|
+
# Example:
|
|
63
|
+
# config.log_file = "log/rails_nexus.log"
|
|
64
|
+
attr_accessor :log_file
|
|
65
|
+
|
|
66
|
+
# Include backtrace in logs (default: true).
|
|
67
|
+
attr_accessor :log_backtrace
|
|
68
|
+
|
|
69
|
+
# Include request params in logs (default: true).
|
|
70
|
+
attr_accessor :log_params
|
|
71
|
+
|
|
72
|
+
# Include user info in logs (default: true).
|
|
73
|
+
attr_accessor :log_user_info
|
|
74
|
+
|
|
75
|
+
# Maximum backtrace lines to include (default: 20).
|
|
76
|
+
attr_accessor :log_backtrace_limit
|
|
77
|
+
|
|
78
|
+
# Enable exception deduplication (default: false).
|
|
79
|
+
# When enabled, only logs unique exception class + controller combinations
|
|
80
|
+
# within a time window.
|
|
81
|
+
attr_accessor :deduplication_enabled
|
|
82
|
+
|
|
83
|
+
# Deduplication time window in seconds (default: 300 = 5 minutes).
|
|
84
|
+
attr_accessor :deduplication_window
|
|
85
|
+
|
|
86
|
+
# ─── Cleanup / Retention ───────────────────────────────────
|
|
87
|
+
|
|
88
|
+
# Automatically delete exceptions older than N days.
|
|
89
|
+
# Set to nil to disable (default: nil).
|
|
90
|
+
attr_accessor :retention_days
|
|
91
|
+
|
|
92
|
+
# ─── Appearance ────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
# Dashboard theme: "light", "dark", or "auto" (default: "auto").
|
|
95
|
+
attr_accessor :theme
|
|
96
|
+
|
|
97
|
+
# Use the host app's layout instead of rails_nexus's built-in layout.
|
|
98
|
+
# Set to true to render inside the host app's sidebar/nav.
|
|
99
|
+
# Set to false (default) for a standalone dashboard.
|
|
100
|
+
attr_accessor :use_host_layout
|
|
101
|
+
|
|
102
|
+
# CSS class added to the dashboard body element.
|
|
103
|
+
attr_accessor :body_class
|
|
104
|
+
|
|
105
|
+
# ─── Sidebar Links ─────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
# Custom links shown in the dashboard sidebar.
|
|
108
|
+
# Example:
|
|
109
|
+
# config.sidebar_links = [
|
|
110
|
+
# { label: "GitHub", url: "https://github.com/myorg/myapp", icon: "code" },
|
|
111
|
+
# { label: "Docs", url: "/docs", icon: "book" }
|
|
112
|
+
# ]
|
|
113
|
+
attr_accessor :sidebar_links
|
|
114
|
+
|
|
115
|
+
# ─── Advanced UI ───────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
# Show stats overview cards on index page (default: true).
|
|
118
|
+
attr_accessor :show_stats
|
|
119
|
+
|
|
120
|
+
# Enable keyboard shortcuts in the dashboard (default: true).
|
|
121
|
+
attr_accessor :keyboard_shortcuts
|
|
122
|
+
|
|
123
|
+
# Enable exception grouping in the list (default: false).
|
|
124
|
+
# Groups exceptions by class + controller.
|
|
125
|
+
attr_accessor :group_exceptions
|
|
126
|
+
|
|
127
|
+
# Show metadata grid in detail view (default: true).
|
|
128
|
+
attr_accessor :show_metadata
|
|
129
|
+
|
|
130
|
+
# Show environment variables in detail view (default: false).
|
|
131
|
+
attr_accessor :show_environment
|
|
132
|
+
|
|
133
|
+
# Show request params in detail view (default: true).
|
|
134
|
+
attr_accessor :show_request
|
|
135
|
+
|
|
136
|
+
# Maximum number of backtrace frames to show before collapsing (default: 30).
|
|
137
|
+
attr_accessor :backtrace_limit
|
|
138
|
+
|
|
139
|
+
# Enable prev/next navigation in detail view (default: true).
|
|
140
|
+
attr_accessor :enable_navigation
|
|
141
|
+
|
|
142
|
+
# Columns to show in the exception table.
|
|
143
|
+
# Default: [:class, :controller, :action, :message, :time]
|
|
144
|
+
attr_accessor :table_columns
|
|
145
|
+
|
|
146
|
+
# Date format for display (default: nil = auto relative).
|
|
147
|
+
attr_accessor :date_format
|
|
148
|
+
|
|
149
|
+
# Rows per page options for the page size selector (default: [25, 50, 100]).
|
|
150
|
+
attr_accessor :page_size_options
|
|
151
|
+
|
|
152
|
+
# Server Statistics
|
|
153
|
+
attr_accessor :show_server_stats
|
|
154
|
+
attr_accessor :show_sidekiq_stats
|
|
155
|
+
attr_accessor :show_process_info
|
|
156
|
+
attr_accessor :stats_refresh_interval
|
|
157
|
+
|
|
158
|
+
# Cron Job Monitoring
|
|
159
|
+
attr_accessor :cron_job_tracking
|
|
160
|
+
attr_accessor :cron_job_retention_days
|
|
161
|
+
|
|
162
|
+
# Webhook Delivery Logging
|
|
163
|
+
attr_accessor :log_webhook_deliveries
|
|
164
|
+
attr_accessor :webhook_delivery_retention_days
|
|
165
|
+
|
|
166
|
+
# Breadcrumbs
|
|
167
|
+
attr_accessor :breadcrumbs_enabled
|
|
168
|
+
|
|
169
|
+
# Storm Protection
|
|
170
|
+
attr_accessor :storm_protection_enabled
|
|
171
|
+
attr_accessor :storm_threshold_per_second
|
|
172
|
+
attr_accessor :storm_cooldown_seconds
|
|
173
|
+
|
|
174
|
+
# User Impact
|
|
175
|
+
attr_accessor :user_impact_tracking
|
|
176
|
+
|
|
177
|
+
# Source Code Viewing
|
|
178
|
+
attr_accessor :source_code_viewing
|
|
179
|
+
|
|
180
|
+
# Backup Management
|
|
181
|
+
attr_accessor :backup_enabled
|
|
182
|
+
attr_accessor :backup_config_path
|
|
183
|
+
attr_accessor :backup_models_path
|
|
184
|
+
attr_accessor :backup_dump_path
|
|
185
|
+
attr_accessor :backup_notify_command
|
|
186
|
+
attr_accessor :backup_alert_threshold_hours
|
|
187
|
+
|
|
188
|
+
# ─── Database ───────────────────────────────────────────────
|
|
189
|
+
# Use a separate database for RailsNexus tables.
|
|
190
|
+
# Options:
|
|
191
|
+
# config.database_url = "mysql2://user:pass@localhost/rails_nexus"
|
|
192
|
+
# config.database_name = "rails_nexus" # uses config/database.yml entry
|
|
193
|
+
# config.database_yml = "config/rails_nexus_database.yml"
|
|
194
|
+
# config.table_prefix = "rails_nexus_"
|
|
195
|
+
attr_accessor :database_url
|
|
196
|
+
attr_accessor :database_name
|
|
197
|
+
attr_accessor :database_yml
|
|
198
|
+
attr_accessor :table_prefix
|
|
199
|
+
|
|
200
|
+
def initialize
|
|
201
|
+
@application_name = nil
|
|
202
|
+
@auth_block = nil
|
|
203
|
+
@exception_data = nil
|
|
204
|
+
@after_create = nil
|
|
205
|
+
@per_page = 30
|
|
206
|
+
@enabled = true
|
|
207
|
+
|
|
208
|
+
# Notifications
|
|
209
|
+
@webhooks = []
|
|
210
|
+
@webhook_timeout = 5
|
|
211
|
+
@webhook_headers = {}
|
|
212
|
+
|
|
213
|
+
# Logging
|
|
214
|
+
@logging_enabled = true
|
|
215
|
+
@log_level = :info
|
|
216
|
+
@log_file = nil
|
|
217
|
+
@log_backtrace = true
|
|
218
|
+
@log_params = true
|
|
219
|
+
@log_user_info = true
|
|
220
|
+
@log_backtrace_limit = 20
|
|
221
|
+
@deduplication_enabled = false
|
|
222
|
+
@deduplication_window = 300
|
|
223
|
+
|
|
224
|
+
# Cleanup
|
|
225
|
+
@retention_days = nil
|
|
226
|
+
|
|
227
|
+
# Appearance
|
|
228
|
+
@theme = "auto"
|
|
229
|
+
@use_host_layout = false
|
|
230
|
+
@body_class = ""
|
|
231
|
+
|
|
232
|
+
# Sidebar
|
|
233
|
+
@sidebar_links = []
|
|
234
|
+
|
|
235
|
+
# Advanced UI
|
|
236
|
+
@show_stats = true
|
|
237
|
+
@keyboard_shortcuts = true
|
|
238
|
+
@group_exceptions = false
|
|
239
|
+
@show_metadata = true
|
|
240
|
+
@show_environment = false
|
|
241
|
+
@show_request = true
|
|
242
|
+
@backtrace_limit = 30
|
|
243
|
+
@enable_navigation = true
|
|
244
|
+
@table_columns = %i[class controller action message time].freeze
|
|
245
|
+
@date_format = nil
|
|
246
|
+
@page_size_options = [25, 50, 100].freeze
|
|
247
|
+
|
|
248
|
+
# Server Statistics
|
|
249
|
+
@show_server_stats = true
|
|
250
|
+
@show_sidekiq_stats = true
|
|
251
|
+
@show_process_info = true
|
|
252
|
+
@stats_refresh_interval = 30
|
|
253
|
+
|
|
254
|
+
# Cron Job Monitoring
|
|
255
|
+
@cron_job_tracking = true
|
|
256
|
+
@cron_job_retention_days = 30
|
|
257
|
+
|
|
258
|
+
# Webhook Delivery Logging
|
|
259
|
+
@log_webhook_deliveries = true
|
|
260
|
+
@webhook_delivery_retention_days = 30
|
|
261
|
+
|
|
262
|
+
# Breadcrumbs (activity trail) — needed for N+1 detection and breadcrumb tab
|
|
263
|
+
@breadcrumbs_enabled = true
|
|
264
|
+
|
|
265
|
+
# Storm Protection (circuit breaker)
|
|
266
|
+
@storm_protection_enabled = true
|
|
267
|
+
@storm_threshold_per_second = 50
|
|
268
|
+
@storm_cooldown_seconds = 60
|
|
269
|
+
|
|
270
|
+
# User Impact
|
|
271
|
+
@user_impact_tracking = true
|
|
272
|
+
|
|
273
|
+
# Source Code Viewing
|
|
274
|
+
@source_code_viewing = true
|
|
275
|
+
|
|
276
|
+
# Backup Management
|
|
277
|
+
@backup_enabled = false
|
|
278
|
+
@backup_config_path = default_backup_config_path
|
|
279
|
+
@backup_models_path = default_backup_models_path
|
|
280
|
+
@backup_dump_path = default_backup_dump_path
|
|
281
|
+
@backup_notify_command = nil
|
|
282
|
+
@backup_alert_threshold_hours = 24
|
|
283
|
+
|
|
284
|
+
# Database
|
|
285
|
+
@database_url = nil
|
|
286
|
+
@database_name = nil
|
|
287
|
+
@database_yml = nil
|
|
288
|
+
@table_prefix = "rails_nexus_"
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
private
|
|
292
|
+
|
|
293
|
+
def default_backup_config_path
|
|
294
|
+
if defined?(Rails) && Rails.respond_to?(:root)
|
|
295
|
+
path = Rails.root.join("config/backup/config.rb")
|
|
296
|
+
path.exist? ? path.to_s : nil
|
|
297
|
+
else
|
|
298
|
+
nil
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def default_backup_models_path
|
|
303
|
+
if defined?(Rails) && Rails.respond_to?(:root)
|
|
304
|
+
path = Rails.root.join("config/backup/models")
|
|
305
|
+
path.exist? ? path.to_s : nil
|
|
306
|
+
else
|
|
307
|
+
nil
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def default_backup_dump_path
|
|
312
|
+
if defined?(Rails) && Rails.respond_to?(:root)
|
|
313
|
+
Rails.root.join("storage/rails_nexus/backups").to_s
|
|
314
|
+
else
|
|
315
|
+
"storage/rails_nexus/backups"
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsNexus
|
|
4
|
+
class Engine < ::Rails::Engine
|
|
5
|
+
isolate_namespace RailsNexus
|
|
6
|
+
|
|
7
|
+
engine_name "rails_nexus"
|
|
8
|
+
|
|
9
|
+
# ─── Asset paths (Propshaft and Sprockets) ───
|
|
10
|
+
|
|
11
|
+
initializer "rails_nexus.assets", before: "propshaft.assets_middleware", group: :all do |app|
|
|
12
|
+
next unless app.config.respond_to?(:assets)
|
|
13
|
+
|
|
14
|
+
app.config.assets.paths << root.join("app/javascript")
|
|
15
|
+
|
|
16
|
+
# Sprockets 4 only compiles assets listed in a manifest or in the
|
|
17
|
+
# precompile list. Register these here so mounting applications do not
|
|
18
|
+
# need to modify their own asset manifest.
|
|
19
|
+
if app.config.assets.respond_to?(:precompile)
|
|
20
|
+
app.config.assets.precompile += %w[
|
|
21
|
+
rails_nexus/application.css
|
|
22
|
+
rails_nexus/application.js
|
|
23
|
+
controllers/rails_nexus_controller.js
|
|
24
|
+
controllers/rails_nexus_detail_controller.js
|
|
25
|
+
controllers/rails_nexus_sidebar_controller.js
|
|
26
|
+
controllers/rails_nexus_theme_controller.js
|
|
27
|
+
]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# ─── Importmap support (for importmap-rails users) ────────────
|
|
32
|
+
|
|
33
|
+
initializer "rails_nexus.importmap", before: "importmap" do |app|
|
|
34
|
+
app.config.importmap.paths << root.join("config/importmap.rb")
|
|
35
|
+
app.config.importmap.cache_sweepers << root.join("app/javascript")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# ─── Database connection (separate DB support) ─────────────────
|
|
39
|
+
|
|
40
|
+
initializer "rails_nexus.database" do
|
|
41
|
+
ActiveSupport.on_load(:active_record) do
|
|
42
|
+
config = RailsNexus.configuration
|
|
43
|
+
|
|
44
|
+
if config.database_url.present?
|
|
45
|
+
uri = URI.parse(config.database_url)
|
|
46
|
+
RailsNexus::BaseRecord.establish_connection(
|
|
47
|
+
adapter: uri.scheme,
|
|
48
|
+
host: uri.host,
|
|
49
|
+
port: uri.port&.to_i,
|
|
50
|
+
username: URI.decode_www_form_component(uri.user || ""),
|
|
51
|
+
password: URI.decode_www_form_component(uri.password || ""),
|
|
52
|
+
database: uri.path&.gsub(%r{^/}, "")
|
|
53
|
+
)
|
|
54
|
+
Rails.logger.info("[RailsNexus] Connected to separate database: #{config.database_url.split("@").last}") if Rails.logger
|
|
55
|
+
|
|
56
|
+
elsif config.database_name.present?
|
|
57
|
+
yaml_path = Rails.root.join("config", "database.yml")
|
|
58
|
+
if yaml_path.exist?
|
|
59
|
+
yaml = YAML.safe_load(ERB.new(yaml_path.read).result, aliases: true) || {}
|
|
60
|
+
env_config = yaml[Rails.env.to_s] || yaml[Rails.env]
|
|
61
|
+
if env_config.is_a?(Hash)
|
|
62
|
+
db_config = env_config.symbolize_keys
|
|
63
|
+
db_config[:database] = config.database_name if config.database_name.is_a?(String)
|
|
64
|
+
RailsNexus::BaseRecord.establish_connection(**db_config)
|
|
65
|
+
Rails.logger.info("[RailsNexus] Connected to database: #{config.database_name}") if Rails.logger
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
elsif config.database_yml.present?
|
|
70
|
+
yaml_path = Rails.root.join(config.database_yml)
|
|
71
|
+
if yaml_path.exist?
|
|
72
|
+
yaml = YAML.safe_load(ERB.new(yaml_path.read).result, aliases: true) || {}
|
|
73
|
+
env_config = yaml[Rails.env.to_s] || yaml[Rails.env]
|
|
74
|
+
RailsNexus::BaseRecord.establish_connection(**env_config.symbolize_keys) if env_config.is_a?(Hash)
|
|
75
|
+
Rails.logger.info("[RailsNexus] Connected to database from #{config.database_yml}") if Rails.logger
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
# If none configured, BaseRecord inherits ApplicationRecord's connection (default)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# ─── Ransack configuration ───────────────────────────────────
|
|
83
|
+
|
|
84
|
+
initializer "rails_nexus.ransack" do
|
|
85
|
+
ActiveSupport.on_load(:active_record) do
|
|
86
|
+
require "rails_nexus/ransack_config" if defined?(Ransack)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# ─── Load support modules ─────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
initializer "rails_nexus.support" do
|
|
93
|
+
require "rails_nexus/notifications"
|
|
94
|
+
require "rails_nexus/cleanup"
|
|
95
|
+
require "rails_nexus/storm_protection"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# ─── Rake tasks ──────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
initializer "rails_nexus.rake_tasks" do
|
|
101
|
+
load "tasks/rails_nexus.rake" if Rake.respond_to?(:application)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# ─── Hook into after_create for notifications ─────────────────
|
|
105
|
+
|
|
106
|
+
initializer "rails_nexus.after_create" do
|
|
107
|
+
ActiveSupport.on_load(:active_record) do
|
|
108
|
+
RailsNexus::LoggedException.after_create do |exception|
|
|
109
|
+
RailsNexus::Notifications.notify(exception)
|
|
110
|
+
rescue StandardError => e
|
|
111
|
+
Rails.logger.error("[RailsNexus] Notification error: #{e.message}")
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# ─── Storm protection ─────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
initializer "rails_nexus.storm_protection" do
|
|
119
|
+
ActiveSupport.on_load(:active_record) do
|
|
120
|
+
require "rails_nexus/storm_protection"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# ─── Breadcrumbs subscription ─────────────────────────────────
|
|
125
|
+
|
|
126
|
+
initializer "rails_nexus.breadcrumbs" do
|
|
127
|
+
ActiveSupport.on_load(:active_record) do
|
|
128
|
+
require "rails_nexus/breadcrumbs"
|
|
129
|
+
if RailsNexus.configuration.breadcrumbs_enabled
|
|
130
|
+
RailsNexus::Breadcrumbs.subscribe!
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsNexus
|
|
4
|
+
class Logger
|
|
5
|
+
# Log levels
|
|
6
|
+
LEVELS = %i[debug info warn error fatal].freeze
|
|
7
|
+
|
|
8
|
+
# Default context that's attached to every log entry
|
|
9
|
+
DEFAULT_CONTEXT = {
|
|
10
|
+
gem: "rails_nexus",
|
|
11
|
+
version: RailsNexus::VERSION
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
# Log an exception with full context
|
|
16
|
+
#
|
|
17
|
+
# RailsNexus::Logger.error(exception, controller: self, extra: { key: "value" })
|
|
18
|
+
#
|
|
19
|
+
def error(exception, controller: nil, extra: {}, **kwargs)
|
|
20
|
+
log(:error, exception, controller: controller, extra: extra, **kwargs)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def warn(exception, controller: nil, extra: {}, **kwargs)
|
|
24
|
+
log(:warn, exception, controller: controller, extra: extra, **kwargs)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def info(message, controller: nil, extra: {}, **kwargs)
|
|
28
|
+
log(:info, message, controller: controller, extra: extra, **kwargs)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def debug(message, controller: nil, extra: {}, **kwargs)
|
|
32
|
+
log(:debug, message, controller: controller, extra: extra, **kwargs)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Log a custom event (not an exception)
|
|
36
|
+
#
|
|
37
|
+
# RailsNexus::Logger.event("user.login", user_id: 123, method: "POST")
|
|
38
|
+
#
|
|
39
|
+
def event(name, data = {})
|
|
40
|
+
entry = build_entry(:info, name, nil, extra: data)
|
|
41
|
+
write_to_output(entry)
|
|
42
|
+
write_to_file(entry) if RailsNexus.configuration.log_file.present?
|
|
43
|
+
entry
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Query logs with filters
|
|
47
|
+
#
|
|
48
|
+
# RailsNexus::Logger.query(level: :error, since: 1.hour.ago, limit: 50)
|
|
49
|
+
#
|
|
50
|
+
def query(level: nil, since: nil, controller_name: nil, limit: 100)
|
|
51
|
+
scope = RailsNexus::LoggedException.order(created_at: :desc)
|
|
52
|
+
scope = scope.where("created_at >= ?", since) if since.present?
|
|
53
|
+
scope = scope.where(exception_class: level.to_s.camelize) if level.present?
|
|
54
|
+
scope = scope.where(controller_name: controller_name) if controller_name.present?
|
|
55
|
+
scope.limit(limit)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Get aggregated stats
|
|
59
|
+
#
|
|
60
|
+
# RailsNexus::Logger.stats(since: 24.hours.ago)
|
|
61
|
+
#
|
|
62
|
+
def stats(since: nil)
|
|
63
|
+
scope = RailsNexus::LoggedException.all
|
|
64
|
+
scope = scope.where("created_at >= ?", since) if since.present?
|
|
65
|
+
|
|
66
|
+
{
|
|
67
|
+
total: scope.count,
|
|
68
|
+
by_class: scope.group(:exception_class).count,
|
|
69
|
+
by_controller: scope.group(:controller_name).count,
|
|
70
|
+
by_hour: scope.group_by { |e| e.created_at.hour }.transform_values(&:count),
|
|
71
|
+
top_exceptions: scope.group(:exception_class).count.sort_by { |_, v| -v }.first(10)
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def log(level, exception_or_message, controller: nil, extra: {}, **kwargs)
|
|
78
|
+
message = exception_or_message.is_a?(Exception) ? exception_or_message.message : exception_or_message.to_s
|
|
79
|
+
exception_class = exception_or_message.is_a?(Exception) ? exception_or_message.class.name : nil
|
|
80
|
+
|
|
81
|
+
entry = build_entry(level, message, exception_class,
|
|
82
|
+
controller: controller,
|
|
83
|
+
extra: extra.merge(kwargs),
|
|
84
|
+
backtrace: exception_or_message.is_a?(Exception) ? exception_or_message.backtrace&.first(20)&.join("\n") : nil
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
write_to_output(entry)
|
|
88
|
+
write_to_file(entry) if RailsNexus.configuration.log_file.present?
|
|
89
|
+
entry
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def build_entry(level, message, exception_class, controller: nil, extra: {}, backtrace: nil)
|
|
93
|
+
entry = DEFAULT_CONTEXT.merge(
|
|
94
|
+
level: level,
|
|
95
|
+
message: message,
|
|
96
|
+
timestamp: Time.current.iso8601
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
entry[:exception_class] = exception_class if exception_class.present?
|
|
100
|
+
|
|
101
|
+
# Add controller context
|
|
102
|
+
if controller.present?
|
|
103
|
+
entry[:controller] = {
|
|
104
|
+
class: controller.class.name,
|
|
105
|
+
action: controller.action_name,
|
|
106
|
+
path: controller.request&.path,
|
|
107
|
+
method: controller.request&.request_method,
|
|
108
|
+
remote_ip: controller.request&.remote_ip,
|
|
109
|
+
user_agent: controller.request&.user_agent
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# Add user context
|
|
113
|
+
if controller.respond_to?(:current_user, true) && controller.current_user.present?
|
|
114
|
+
entry[:user] = {
|
|
115
|
+
id: controller.current_user.id,
|
|
116
|
+
email: controller.current_user.email
|
|
117
|
+
}
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Add request params (filtered)
|
|
121
|
+
if controller.request.present?
|
|
122
|
+
entry[:params] = filter_params(controller.request.filtered_parameters)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Add extra metadata
|
|
127
|
+
entry[:extra] = extra if extra.present?
|
|
128
|
+
entry[:backtrace] = backtrace if backtrace.present?
|
|
129
|
+
|
|
130
|
+
entry
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def write_to_output(entry)
|
|
134
|
+
level = entry[:level]
|
|
135
|
+
color = case level
|
|
136
|
+
when :debug then "\e[37m" # white
|
|
137
|
+
when :info then "\e[36m" # cyan
|
|
138
|
+
when :warn then "\e[33m" # yellow
|
|
139
|
+
when :error then "\e[31m" # red
|
|
140
|
+
when :fatal then "\e[35m" # magenta
|
|
141
|
+
else "\e[0m"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
reset = "\e[0m"
|
|
145
|
+
ts = entry[:timestamp].sub(/T/, " ").sub(/\+.*/, "")
|
|
146
|
+
msg = entry[:message]
|
|
147
|
+
cls = entry[:exception_class]
|
|
148
|
+
|
|
149
|
+
line = "#{color}[RailsNexus]#{reset} #{ts} #{level.to_s.upcase.ljust(5)}"
|
|
150
|
+
line += " #{cls}:" if cls.present?
|
|
151
|
+
line += " #{msg}"
|
|
152
|
+
|
|
153
|
+
if entry[:controller]
|
|
154
|
+
ctrl = entry[:controller]
|
|
155
|
+
line += " [#{ctrl[:class]}##{ctrl[:action]}]"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
Rails.logger.send(level, line)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def write_to_file(entry)
|
|
162
|
+
file = RailsNexus.configuration.log_file
|
|
163
|
+
return unless file.present?
|
|
164
|
+
|
|
165
|
+
File.open(file, "a") do |f|
|
|
166
|
+
f.puts(entry.to_json)
|
|
167
|
+
end
|
|
168
|
+
rescue StandardError => e
|
|
169
|
+
Rails.logger.error("[RailsNexus] Failed to write to log file: #{e.message}")
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def filter_params(params)
|
|
173
|
+
return {} unless params.is_a?(Hash)
|
|
174
|
+
|
|
175
|
+
rails_filters = Rails.application.config.filter_parameters
|
|
176
|
+
params.each_with_object({}) do |(key, value), result|
|
|
177
|
+
if rails_filters.any? { |f| key.to_s.include?(f.to_s) }
|
|
178
|
+
result[key] = "[FILTERED]"
|
|
179
|
+
elsif value.is_a?(Hash)
|
|
180
|
+
result[key] = filter_params(value)
|
|
181
|
+
else
|
|
182
|
+
result[key] = value
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|