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.
Files changed (114) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +430 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/config/rails_nexus_manifest.js +2 -0
  6. data/app/assets/javascripts/rails_nexus/controllers.js +603 -0
  7. data/app/assets/stylesheets/rails_nexus/application.css +1964 -0
  8. data/app/controllers/rails_nexus/analytics_controller.rb +361 -0
  9. data/app/controllers/rails_nexus/application_controller.rb +15 -0
  10. data/app/controllers/rails_nexus/backup_controller.rb +91 -0
  11. data/app/controllers/rails_nexus/cron_jobs_controller.rb +64 -0
  12. data/app/controllers/rails_nexus/database_health_controller.rb +128 -0
  13. data/app/controllers/rails_nexus/logged_exceptions_controller.rb +227 -0
  14. data/app/controllers/rails_nexus/n1_patterns_controller.rb +10 -0
  15. data/app/controllers/rails_nexus/settings_controller.rb +100 -0
  16. data/app/controllers/rails_nexus/source_code_controller.rb +39 -0
  17. data/app/controllers/rails_nexus/stats_controller.rb +494 -0
  18. data/app/controllers/rails_nexus/workflow_controller.rb +96 -0
  19. data/app/helpers/rails_nexus/application_helper.rb +5 -0
  20. data/app/helpers/rails_nexus/logged_exceptions_helper.rb +254 -0
  21. data/app/helpers/rails_nexus/source_code_helper.rb +107 -0
  22. data/app/javascript/controllers/rails_nexus_controller.js +380 -0
  23. data/app/javascript/controllers/rails_nexus_detail_controller.js +165 -0
  24. data/app/javascript/controllers/rails_nexus_sidebar_controller.js +15 -0
  25. data/app/javascript/controllers/rails_nexus_theme_controller.js +31 -0
  26. data/app/javascript/rails_nexus/application.js +17 -0
  27. data/app/jobs/rails_nexus/application_job.rb +4 -0
  28. data/app/mailers/rails_nexus/application_mailer.rb +6 -0
  29. data/app/models/rails_nexus/application_record.rb +5 -0
  30. data/app/models/rails_nexus/backup.rb +107 -0
  31. data/app/models/rails_nexus/base_record.rb +26 -0
  32. data/app/models/rails_nexus/comment.rb +13 -0
  33. data/app/models/rails_nexus/cron_job.rb +72 -0
  34. data/app/models/rails_nexus/database_stat.rb +128 -0
  35. data/app/models/rails_nexus/event.rb +101 -0
  36. data/app/models/rails_nexus/logged_exception.rb +864 -0
  37. data/app/models/rails_nexus/logged_exception_n1.rb +87 -0
  38. data/app/models/rails_nexus/metric.rb +107 -0
  39. data/app/models/rails_nexus/nginx_metric.rb +112 -0
  40. data/app/models/rails_nexus/server_metric.rb +168 -0
  41. data/app/models/rails_nexus/webhook_delivery.rb +64 -0
  42. data/app/services/rails_nexus/backup_service.rb +275 -0
  43. data/app/views/layouts/rails_nexus/application.html.erb +185 -0
  44. data/app/views/rails_nexus/analytics/index.html.erb +363 -0
  45. data/app/views/rails_nexus/backup/files.html.erb +53 -0
  46. data/app/views/rails_nexus/backup/index.html.erb +211 -0
  47. data/app/views/rails_nexus/backup/settings.html.erb +158 -0
  48. data/app/views/rails_nexus/cron_jobs/index.html.erb +141 -0
  49. data/app/views/rails_nexus/database_health/index.html.erb +139 -0
  50. data/app/views/rails_nexus/logged_exceptions/_comments_list.html.erb +50 -0
  51. data/app/views/rails_nexus/logged_exceptions/_exceptions.html.erb +190 -0
  52. data/app/views/rails_nexus/logged_exceptions/_feed.html.erb +3 -0
  53. data/app/views/rails_nexus/logged_exceptions/_filter_group.html.erb +10 -0
  54. data/app/views/rails_nexus/logged_exceptions/_show.html.erb +554 -0
  55. data/app/views/rails_nexus/logged_exceptions/_workflow_panel.html.erb +118 -0
  56. data/app/views/rails_nexus/logged_exceptions/clear.turbo_stream.erb +6 -0
  57. data/app/views/rails_nexus/logged_exceptions/destroy.turbo_stream.erb +2 -0
  58. data/app/views/rails_nexus/logged_exceptions/destroy_all.turbo_stream.erb +6 -0
  59. data/app/views/rails_nexus/logged_exceptions/feed.rss.builder +20 -0
  60. data/app/views/rails_nexus/logged_exceptions/index.html.erb +323 -0
  61. data/app/views/rails_nexus/logged_exceptions/query.html.erb +3 -0
  62. data/app/views/rails_nexus/logged_exceptions/query.turbo_stream.erb +6 -0
  63. data/app/views/rails_nexus/logged_exceptions/show.html.erb +3 -0
  64. data/app/views/rails_nexus/logged_exceptions/show.turbo_stream.erb +5 -0
  65. data/app/views/rails_nexus/n1_patterns/index.html.erb +153 -0
  66. data/app/views/rails_nexus/settings/index.html.erb +140 -0
  67. data/app/views/rails_nexus/stats/index.html.erb +569 -0
  68. data/config/importmap.rb +15 -0
  69. data/config/initializers/date_formats.rb +5 -0
  70. data/config/initializers/rails_nexus.rb +18 -0
  71. data/config/locales/en.yml +47 -0
  72. data/config/routes.rb +58 -0
  73. data/db/migrate/20240330122311_create_rails_nexus_logged_exceptions.rb +22 -0
  74. data/lib/generators/rails_nexus/backup_generator.rb +70 -0
  75. data/lib/generators/rails_nexus/customize_generator.rb +159 -0
  76. data/lib/generators/rails_nexus/install_generator.rb +61 -0
  77. data/lib/generators/rails_nexus/templates/INSTALL.md +9 -0
  78. data/lib/generators/rails_nexus/templates/backup/backup_helper.sh +152 -0
  79. data/lib/generators/rails_nexus/templates/backup/config.rb +27 -0
  80. data/lib/generators/rails_nexus/templates/backup/models/daily_backup.rb +24 -0
  81. data/lib/generators/rails_nexus/templates/backup/models/full_backup.rb +35 -0
  82. data/lib/generators/rails_nexus/templates/backup/models/sync_backup.rb +43 -0
  83. data/lib/generators/rails_nexus/templates/backup/mysql-config/db_config.cnf +15 -0
  84. data/lib/generators/rails_nexus/templates/backup/schedule.rb +28 -0
  85. data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_controller.js +310 -0
  86. data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_detail_controller.js +98 -0
  87. data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_sidebar_controller.js +15 -0
  88. data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_theme_controller.js +30 -0
  89. data/lib/generators/rails_nexus/templates/migration.rb +24 -0
  90. data/lib/generators/rails_nexus/templates/migration_advanced_features.rb +30 -0
  91. data/lib/generators/rails_nexus/templates/migration_cron_jobs.rb +22 -0
  92. data/lib/generators/rails_nexus/templates/migration_new_schema.rb +120 -0
  93. data/lib/generators/rails_nexus/templates/migration_platform_detection.rb +11 -0
  94. data/lib/generators/rails_nexus/templates/migration_webhook_deliveries.rb +21 -0
  95. data/lib/generators/rails_nexus/templates/migration_workflow.rb +29 -0
  96. data/lib/generators/rails_nexus/templates/rails_nexus.rb +96 -0
  97. data/lib/generators/rails_nexus/templates/views/layouts/rails_nexus/application.html.erb +129 -0
  98. data/lib/generators/rails_nexus/templates/views/rails_nexus/logged_exceptions/_exceptions.html.erb +123 -0
  99. data/lib/generators/rails_nexus/templates/views/rails_nexus/logged_exceptions/_show.html.erb +297 -0
  100. data/lib/generators/rails_nexus/templates/views/rails_nexus/logged_exceptions/index.html.erb +113 -0
  101. data/lib/rails_nexus/breadcrumbs.rb +161 -0
  102. data/lib/rails_nexus/cleanup.rb +32 -0
  103. data/lib/rails_nexus/configuration.rb +319 -0
  104. data/lib/rails_nexus/engine.rb +135 -0
  105. data/lib/rails_nexus/logger.rb +188 -0
  106. data/lib/rails_nexus/notifications.rb +105 -0
  107. data/lib/rails_nexus/ransack_config.rb +4 -0
  108. data/lib/rails_nexus/storm_protection.rb +124 -0
  109. data/lib/rails_nexus/version.rb +3 -0
  110. data/lib/rails_nexus.rb +133 -0
  111. data/lib/tasks/rails_nexus.rake +230 -0
  112. data/lib/tasks/rails_nexus_tasks.rake +37 -0
  113. data/rails_nexus.gemspec +36 -0
  114. 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