mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 01:31:39 -05:00
Add RailsPulse
This commit is contained in:
parent
14a0bb6478
commit
be5307f963
9 changed files with 491 additions and 1 deletions
1
Gemfile
1
Gemfile
|
|
@ -36,6 +36,7 @@ gem 'puma'
|
||||||
gem 'pundit', '>= 2.5.1'
|
gem 'pundit', '>= 2.5.1'
|
||||||
gem 'rails', '~> 8.0'
|
gem 'rails', '~> 8.0'
|
||||||
gem 'rails_icons'
|
gem 'rails_icons'
|
||||||
|
gem 'rails_pulse'
|
||||||
gem 'redis'
|
gem 'redis'
|
||||||
gem 'rexml'
|
gem 'rexml'
|
||||||
gem 'rgeo'
|
gem 'rgeo'
|
||||||
|
|
|
||||||
18
Gemfile.lock
18
Gemfile.lock
|
|
@ -141,6 +141,7 @@ GEM
|
||||||
cronex (0.15.0)
|
cronex (0.15.0)
|
||||||
tzinfo
|
tzinfo
|
||||||
unicode (>= 0.4.4.5)
|
unicode (>= 0.4.4.5)
|
||||||
|
css-zero (1.1.15)
|
||||||
csv (3.3.4)
|
csv (3.3.4)
|
||||||
data_migrate (11.3.1)
|
data_migrate (11.3.1)
|
||||||
activerecord (>= 6.1)
|
activerecord (>= 6.1)
|
||||||
|
|
@ -351,6 +352,9 @@ GEM
|
||||||
optimist (3.2.1)
|
optimist (3.2.1)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
ostruct (0.6.1)
|
ostruct (0.6.1)
|
||||||
|
pagy (43.2.2)
|
||||||
|
json
|
||||||
|
yaml
|
||||||
parallel (1.27.0)
|
parallel (1.27.0)
|
||||||
parser (3.3.9.0)
|
parser (3.3.9.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
|
|
@ -429,6 +433,14 @@ GEM
|
||||||
rails_icons (1.4.0)
|
rails_icons (1.4.0)
|
||||||
nokogiri (~> 1.16, >= 1.16.4)
|
nokogiri (~> 1.16, >= 1.16.4)
|
||||||
rails (> 6.1)
|
rails (> 6.1)
|
||||||
|
rails_pulse (0.2.4)
|
||||||
|
css-zero (~> 1.1, >= 1.1.4)
|
||||||
|
groupdate (~> 6.0)
|
||||||
|
pagy (>= 8, < 44)
|
||||||
|
rails (>= 7.1.0, < 9.0.0)
|
||||||
|
ransack (~> 4.0)
|
||||||
|
request_store (~> 1.5)
|
||||||
|
turbo-rails (~> 2.0.11)
|
||||||
railties (8.0.3)
|
railties (8.0.3)
|
||||||
actionpack (= 8.0.3)
|
actionpack (= 8.0.3)
|
||||||
activesupport (= 8.0.3)
|
activesupport (= 8.0.3)
|
||||||
|
|
@ -440,6 +452,10 @@ GEM
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.3.1)
|
rake (13.3.1)
|
||||||
|
ransack (4.4.1)
|
||||||
|
activerecord (>= 7.2)
|
||||||
|
activesupport (>= 7.2)
|
||||||
|
i18n
|
||||||
rdoc (6.16.1)
|
rdoc (6.16.1)
|
||||||
erb
|
erb
|
||||||
psych (>= 4.0.0)
|
psych (>= 4.0.0)
|
||||||
|
|
@ -625,6 +641,7 @@ GEM
|
||||||
zeitwerk (>= 2.7)
|
zeitwerk (>= 2.7)
|
||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
|
yaml (0.4.0)
|
||||||
zeitwerk (2.7.3)
|
zeitwerk (2.7.3)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
|
|
@ -677,6 +694,7 @@ DEPENDENCIES
|
||||||
pundit (>= 2.5.1)
|
pundit (>= 2.5.1)
|
||||||
rails (~> 8.0)
|
rails (~> 8.0)
|
||||||
rails_icons
|
rails_icons
|
||||||
|
rails_pulse
|
||||||
redis
|
redis
|
||||||
rexml
|
rexml
|
||||||
rgeo
|
rgeo
|
||||||
|
|
|
||||||
205
config/initializers/rails_pulse.rb
Normal file
205
config/initializers/rails_pulse.rb
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
RailsPulse.configure do |config|
|
||||||
|
# ====================================================================================================
|
||||||
|
# GLOBAL CONFIGURATION
|
||||||
|
# ====================================================================================================
|
||||||
|
|
||||||
|
# Enable or disable Rails Pulse
|
||||||
|
config.enabled = true
|
||||||
|
|
||||||
|
# ====================================================================================================
|
||||||
|
# THRESHOLDS
|
||||||
|
# ====================================================================================================
|
||||||
|
# These thresholds are used to determine if a route, request, or query is slow, very slow, or critical.
|
||||||
|
# Values are in milliseconds (ms). Adjust these based on your application's performance requirements.
|
||||||
|
|
||||||
|
# Thresholds for an individual route
|
||||||
|
config.route_thresholds = {
|
||||||
|
slow: 500,
|
||||||
|
very_slow: 1500,
|
||||||
|
critical: 3000
|
||||||
|
}
|
||||||
|
|
||||||
|
# Thresholds for an individual request
|
||||||
|
config.request_thresholds = {
|
||||||
|
slow: 700,
|
||||||
|
very_slow: 2000,
|
||||||
|
critical: 4000
|
||||||
|
}
|
||||||
|
|
||||||
|
# Thresholds for an individual database query
|
||||||
|
config.query_thresholds = {
|
||||||
|
slow: 100,
|
||||||
|
very_slow: 500,
|
||||||
|
critical: 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
# ====================================================================================================
|
||||||
|
# FILTERING
|
||||||
|
# ====================================================================================================
|
||||||
|
|
||||||
|
# Asset Tracking Configuration
|
||||||
|
# By default, Rails Pulse ignores asset requests (images, CSS, JS files) to focus on application performance.
|
||||||
|
# Set track_assets to true if you want to monitor asset delivery performance.
|
||||||
|
config.track_assets = false
|
||||||
|
|
||||||
|
# Custom asset patterns to ignore (in addition to the built-in defaults)
|
||||||
|
# Only applies when track_assets is false. Add patterns for app-specific asset paths.
|
||||||
|
config.custom_asset_patterns = [
|
||||||
|
# Example: ignore specific asset directories
|
||||||
|
# %r{^/uploads/},
|
||||||
|
# %r{^/media/},
|
||||||
|
# "/special-assets/"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Rails Pulse Mount Path (optional)
|
||||||
|
# If Rails Pulse is mounted at a custom path, specify it here to prevent
|
||||||
|
# Rails Pulse from tracking its own requests. Leave as nil for default '/rails_pulse'.
|
||||||
|
# Examples:
|
||||||
|
# config.mount_path = "/admin/monitoring"
|
||||||
|
config.mount_path = nil
|
||||||
|
|
||||||
|
# Manual route filtering
|
||||||
|
# Specify additional routes, requests, or queries to ignore from performance tracking.
|
||||||
|
# Each array can include strings (exact matches) or regular expressions.
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# config.ignored_routes = ["/health_check", %r{^/admin}]
|
||||||
|
# config.ignored_requests = ["GET /status", %r{POST /api/v1/.*}]
|
||||||
|
# config.ignored_queries = ["SELECT 1", %r{FROM \"schema_migrations\"}]
|
||||||
|
|
||||||
|
config.ignored_routes = []
|
||||||
|
config.ignored_requests = []
|
||||||
|
config.ignored_queries = []
|
||||||
|
|
||||||
|
# ====================================================================================================
|
||||||
|
# TAGGING
|
||||||
|
# ====================================================================================================
|
||||||
|
# Define custom tags for categorizing routes, requests, and queries.
|
||||||
|
# You can add any custom tags you want for filtering and organization.
|
||||||
|
#
|
||||||
|
# Tag names should be in present tense and describe the current state or category.
|
||||||
|
# Examples of good tag names:
|
||||||
|
# - "critical" (for high-priority endpoints)
|
||||||
|
# - "experimental" (for routes under development)
|
||||||
|
# - "deprecated" (for routes being phased out)
|
||||||
|
# - "external" (for third-party API calls)
|
||||||
|
# - "background" (for async job-related operations)
|
||||||
|
# - "admin" (for administrative routes)
|
||||||
|
# - "public" (for public-facing routes)
|
||||||
|
#
|
||||||
|
# Example configuration:
|
||||||
|
# config.tags = ["ignored", "critical", "experimental", "deprecated", "external", "admin"]
|
||||||
|
|
||||||
|
config.tags = %w[ignored critical experimental]
|
||||||
|
|
||||||
|
# ====================================================================================================
|
||||||
|
# DATABASE CONFIGURATION
|
||||||
|
# ====================================================================================================
|
||||||
|
# Configure Rails Pulse to use a separate database for performance monitoring data.
|
||||||
|
# This is optional but recommended for production applications to isolate performance
|
||||||
|
# data from your main application database.
|
||||||
|
#
|
||||||
|
# Uncomment and configure one of the following patterns:
|
||||||
|
|
||||||
|
# Option 1: Separate single database for Rails Pulse
|
||||||
|
# config.connects_to = {
|
||||||
|
# database: { writing: :rails_pulse, reading: :rails_pulse }
|
||||||
|
# }
|
||||||
|
|
||||||
|
# Option 2: Primary/replica configuration for Rails Pulse
|
||||||
|
# config.connects_to = {
|
||||||
|
# database: { writing: :rails_pulse_primary, reading: :rails_pulse_replica }
|
||||||
|
# }
|
||||||
|
|
||||||
|
# Don't forget to add the database configuration to config/database.yml:
|
||||||
|
#
|
||||||
|
# production:
|
||||||
|
# # ... your main database config ...
|
||||||
|
# rails_pulse:
|
||||||
|
# adapter: postgresql # or mysql2, sqlite3
|
||||||
|
# database: myapp_rails_pulse_production
|
||||||
|
# username: rails_pulse_user
|
||||||
|
# password: <%= Rails.application.credentials.dig(:rails_pulse, :database_password) %>
|
||||||
|
# host: localhost
|
||||||
|
# pool: 5
|
||||||
|
|
||||||
|
# ====================================================================================================
|
||||||
|
# AUTHENTICATION
|
||||||
|
# ====================================================================================================
|
||||||
|
# Configure authentication to secure access to the Rails Pulse dashboard.
|
||||||
|
# Authentication is ENABLED BY DEFAULT in production environments for security.
|
||||||
|
#
|
||||||
|
# If no authentication method is configured, Rails Pulse will use HTTP Basic Auth
|
||||||
|
# with credentials from RAILS_PULSE_USERNAME (default: 'admin') and RAILS_PULSE_PASSWORD
|
||||||
|
# environment variables. Set RAILS_PULSE_PASSWORD to enable this fallback.
|
||||||
|
#
|
||||||
|
# Uncomment and configure one of the following patterns based on your authentication system:
|
||||||
|
|
||||||
|
# Enable/disable authentication (enabled by default in production)
|
||||||
|
config.authentication_enabled = true
|
||||||
|
|
||||||
|
# Where to redirect unauthorized users
|
||||||
|
# config.authentication_redirect_path = "/"
|
||||||
|
|
||||||
|
# Custom authentication method - choose one of the examples below:
|
||||||
|
|
||||||
|
# Example 1: Devise with admin role check
|
||||||
|
config.authentication_method = proc {
|
||||||
|
redirect_to main_app.root_path, alert: 'Access denied' unless user_signed_in? && current_user.admin?
|
||||||
|
}
|
||||||
|
|
||||||
|
# Example 2: Custom session-based authentication
|
||||||
|
# config.authentication_method = proc {
|
||||||
|
# unless session[:user_id] && User.find_by(id: session[:user_id])&.admin?
|
||||||
|
# redirect_to main_app.login_path, alert: "Please log in as an admin"
|
||||||
|
# end
|
||||||
|
# }
|
||||||
|
|
||||||
|
# Example 3: Warden authentication
|
||||||
|
# config.authentication_method = proc {
|
||||||
|
# warden.authenticate!(:scope => :admin)
|
||||||
|
# }
|
||||||
|
|
||||||
|
# Example 4: Basic HTTP authentication
|
||||||
|
# config.authentication_method = proc {
|
||||||
|
# authenticate_or_request_with_http_basic do |username, password|
|
||||||
|
# username == ENV['RAILS_PULSE_USERNAME'] && password == ENV['RAILS_PULSE_PASSWORD']
|
||||||
|
# end
|
||||||
|
# }
|
||||||
|
|
||||||
|
# Example 5: Custom authorization check
|
||||||
|
# config.authentication_method = proc {
|
||||||
|
# current_user = User.find_by(id: session[:user_id])
|
||||||
|
# unless current_user&.can_access_rails_pulse?
|
||||||
|
# render plain: "Forbidden", status: :forbidden
|
||||||
|
# end
|
||||||
|
# }
|
||||||
|
|
||||||
|
# ====================================================================================================
|
||||||
|
# DATA CLEANUP
|
||||||
|
# ====================================================================================================
|
||||||
|
# Configure automatic cleanup of old performance data to manage database size.
|
||||||
|
# Rails Pulse provides two cleanup mechanisms that work together:
|
||||||
|
#
|
||||||
|
# 1. Time-based cleanup: Delete records older than the retention period
|
||||||
|
# 2. Count-based cleanup: Keep only the specified number of records per table
|
||||||
|
#
|
||||||
|
# Cleanup order respects foreign key constraints:
|
||||||
|
# operations → requests → queries/routes
|
||||||
|
|
||||||
|
# Enable or disable automatic data cleanup
|
||||||
|
config.archiving_enabled = true
|
||||||
|
|
||||||
|
# Time-based retention - delete records older than this period
|
||||||
|
config.full_retention_period = 2.weeks
|
||||||
|
|
||||||
|
# Count-based retention - maximum records to keep per table
|
||||||
|
# After time-based cleanup, if tables still exceed these limits,
|
||||||
|
# the oldest remaining records will be deleted to stay under the limit
|
||||||
|
config.max_table_records = {
|
||||||
|
rails_pulse_requests: 10_000, # HTTP requests (moderate volume)
|
||||||
|
rails_pulse_operations: 50_000, # Operations within requests (high volume)
|
||||||
|
rails_pulse_routes: 1000, # Unique routes (low volume)
|
||||||
|
rails_pulse_queries: 500 # Normalized SQL queries (low volume)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
@ -26,6 +26,7 @@ Rails.application.routes.draw do
|
||||||
} do
|
} do
|
||||||
mount Sidekiq::Web => '/sidekiq'
|
mount Sidekiq::Web => '/sidekiq'
|
||||||
end
|
end
|
||||||
|
mount RailsPulse::Engine => '/rails_pulse'
|
||||||
|
|
||||||
# We want to return a nice error message if the user is not authorized to access Sidekiq
|
# We want to return a nice error message if the user is not authorized to access Sidekiq
|
||||||
match '/sidekiq' => redirect { |_, request|
|
match '/sidekiq' => redirect { |_, request|
|
||||||
|
|
|
||||||
|
|
@ -49,3 +49,13 @@ nightly_family_invitations_cleanup_job:
|
||||||
cron: "30 2 * * *" # every day at 02:30
|
cron: "30 2 * * *" # every day at 02:30
|
||||||
class: "Family::Invitations::CleanupJob"
|
class: "Family::Invitations::CleanupJob"
|
||||||
queue: family
|
queue: family
|
||||||
|
|
||||||
|
rails_pulse_summary_job:
|
||||||
|
cron: "5 * * * *" # every hour at 5 minutes past the hour
|
||||||
|
class: "RailsPulse::SummaryJob"
|
||||||
|
queue: default
|
||||||
|
|
||||||
|
rails_pulse_clean_up_job:
|
||||||
|
cron: "0 1 * * *" # every day at 01:00
|
||||||
|
class: "RailsPulse::CleanupJob"
|
||||||
|
queue: default
|
||||||
|
|
|
||||||
23
db/migrate/20251228163703_install_rails_pulse_tables.rb
Normal file
23
db/migrate/20251228163703_install_rails_pulse_tables.rb
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated from Rails Pulse schema - automatically loads current schema definition
|
||||||
|
class InstallRailsPulseTables < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
# Load and execute the Rails Pulse schema directly
|
||||||
|
# This ensures the migration is always in sync with the schema file
|
||||||
|
schema_file = File.join(::Rails.root.to_s, "db/rails_pulse_schema.rb")
|
||||||
|
|
||||||
|
if File.exist?(schema_file)
|
||||||
|
say "Loading Rails Pulse schema from db/rails_pulse_schema.rb"
|
||||||
|
|
||||||
|
# Load the schema file to define RailsPulse::Schema
|
||||||
|
load schema_file
|
||||||
|
|
||||||
|
# Execute the schema in the context of this migration
|
||||||
|
RailsPulse::Schema.call(connection)
|
||||||
|
|
||||||
|
say "Rails Pulse tables created successfully"
|
||||||
|
say "The schema file db/rails_pulse_schema.rb remains as your single source of truth"
|
||||||
|
else
|
||||||
|
raise "Rails Pulse schema file not found at db/rails_pulse_schema.rb"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
0
db/rails_pulse_migrate/.keep
Normal file
0
db/rails_pulse_migrate/.keep
Normal file
133
db/rails_pulse_schema.rb
Normal file
133
db/rails_pulse_schema.rb
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
# Rails Pulse Database Schema
|
||||||
|
# This file contains the complete schema for Rails Pulse tables
|
||||||
|
# Load with: rails db:schema:load:rails_pulse or db:prepare
|
||||||
|
|
||||||
|
RailsPulse::Schema = lambda do |connection|
|
||||||
|
# Skip if all tables already exist to prevent conflicts
|
||||||
|
required_tables = [ :rails_pulse_routes, :rails_pulse_queries, :rails_pulse_requests, :rails_pulse_operations, :rails_pulse_summaries ]
|
||||||
|
|
||||||
|
if ENV["CI"] == "true"
|
||||||
|
existing_tables = required_tables.select { |table| connection.table_exists?(table) }
|
||||||
|
missing_tables = required_tables - existing_tables
|
||||||
|
puts "[RailsPulse::Schema] Existing tables: #{existing_tables.join(', ')}" if existing_tables.any?
|
||||||
|
puts "[RailsPulse::Schema] Missing tables: #{missing_tables.join(', ')}" if missing_tables.any?
|
||||||
|
end
|
||||||
|
|
||||||
|
return if required_tables.all? { |table| connection.table_exists?(table) }
|
||||||
|
|
||||||
|
connection.create_table :rails_pulse_routes do |t|
|
||||||
|
t.string :method, null: false, comment: "HTTP method (e.g., GET, POST)"
|
||||||
|
t.string :path, null: false, comment: "Request path (e.g., /posts/index)"
|
||||||
|
t.text :tags, comment: "JSON array of tags for filtering and categorization"
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
connection.add_index :rails_pulse_routes, [ :method, :path ], unique: true, name: "index_rails_pulse_routes_on_method_and_path"
|
||||||
|
|
||||||
|
connection.create_table :rails_pulse_queries do |t|
|
||||||
|
t.string :normalized_sql, limit: 1000, null: false, comment: "Normalized SQL query string (e.g., SELECT * FROM users WHERE id = ?)"
|
||||||
|
t.datetime :analyzed_at, comment: "When query analysis was last performed"
|
||||||
|
t.text :explain_plan, comment: "EXPLAIN output from actual SQL execution"
|
||||||
|
t.text :issues, comment: "JSON array of detected performance issues"
|
||||||
|
t.text :metadata, comment: "JSON object containing query complexity metrics"
|
||||||
|
t.text :query_stats, comment: "JSON object with query characteristics analysis"
|
||||||
|
t.text :backtrace_analysis, comment: "JSON object with call chain and N+1 detection"
|
||||||
|
t.text :index_recommendations, comment: "JSON array of database index recommendations"
|
||||||
|
t.text :n_plus_one_analysis, comment: "JSON object with enhanced N+1 query detection results"
|
||||||
|
t.text :suggestions, comment: "JSON array of optimization recommendations"
|
||||||
|
t.text :tags, comment: "JSON array of tags for filtering and categorization"
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
connection.add_index :rails_pulse_queries, :normalized_sql, unique: true, name: "index_rails_pulse_queries_on_normalized_sql", length: 191
|
||||||
|
|
||||||
|
connection.create_table :rails_pulse_requests do |t|
|
||||||
|
t.references :route, null: false, foreign_key: { to_table: :rails_pulse_routes }, comment: "Link to the route"
|
||||||
|
t.decimal :duration, precision: 15, scale: 6, null: false, comment: "Total request duration in milliseconds"
|
||||||
|
t.integer :status, null: false, comment: "HTTP status code (e.g., 200, 500)"
|
||||||
|
t.boolean :is_error, null: false, default: false, comment: "True if status >= 500"
|
||||||
|
t.string :request_uuid, null: false, comment: "Unique identifier for the request (e.g., UUID)"
|
||||||
|
t.string :controller_action, comment: "Controller and action handling the request (e.g., PostsController#show)"
|
||||||
|
t.timestamp :occurred_at, null: false, comment: "When the request started"
|
||||||
|
t.text :tags, comment: "JSON array of tags for filtering and categorization"
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
connection.add_index :rails_pulse_requests, :occurred_at, name: "index_rails_pulse_requests_on_occurred_at"
|
||||||
|
connection.add_index :rails_pulse_requests, :request_uuid, unique: true, name: "index_rails_pulse_requests_on_request_uuid"
|
||||||
|
connection.add_index :rails_pulse_requests, [ :route_id, :occurred_at ], name: "index_rails_pulse_requests_on_route_id_and_occurred_at"
|
||||||
|
|
||||||
|
connection.create_table :rails_pulse_operations do |t|
|
||||||
|
t.references :request, null: false, foreign_key: { to_table: :rails_pulse_requests }, comment: "Link to the request"
|
||||||
|
t.references :query, foreign_key: { to_table: :rails_pulse_queries }, index: true, comment: "Link to the normalized SQL query"
|
||||||
|
t.string :operation_type, null: false, comment: "Type of operation (e.g., database, view, gem_call)"
|
||||||
|
t.string :label, null: false, comment: "Descriptive name (e.g., SELECT FROM users WHERE id = 1, render layout)"
|
||||||
|
t.decimal :duration, precision: 15, scale: 6, null: false, comment: "Operation duration in milliseconds"
|
||||||
|
t.string :codebase_location, comment: "File and line number (e.g., app/models/user.rb:25)"
|
||||||
|
t.float :start_time, null: false, default: 0.0, comment: "Operation start time in milliseconds"
|
||||||
|
t.timestamp :occurred_at, null: false, comment: "When the request started"
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
connection.add_index :rails_pulse_operations, :operation_type, name: "index_rails_pulse_operations_on_operation_type"
|
||||||
|
connection.add_index :rails_pulse_operations, :occurred_at, name: "index_rails_pulse_operations_on_occurred_at"
|
||||||
|
connection.add_index :rails_pulse_operations, [ :query_id, :occurred_at ], name: "index_rails_pulse_operations_on_query_and_time"
|
||||||
|
connection.add_index :rails_pulse_operations, [ :query_id, :duration, :occurred_at ], name: "index_rails_pulse_operations_query_performance"
|
||||||
|
connection.add_index :rails_pulse_operations, [ :occurred_at, :duration, :operation_type ], name: "index_rails_pulse_operations_on_time_duration_type"
|
||||||
|
|
||||||
|
connection.create_table :rails_pulse_summaries do |t|
|
||||||
|
# Time fields
|
||||||
|
t.datetime :period_start, null: false, comment: "Start of the aggregation period"
|
||||||
|
t.datetime :period_end, null: false, comment: "End of the aggregation period"
|
||||||
|
t.string :period_type, null: false, comment: "Aggregation period type: hour, day, week, month"
|
||||||
|
|
||||||
|
# Polymorphic association to handle both routes and queries
|
||||||
|
t.references :summarizable, polymorphic: true, null: false, index: true, comment: "Link to Route or Query"
|
||||||
|
# This creates summarizable_type (e.g., 'RailsPulse::Route', 'RailsPulse::Query')
|
||||||
|
# and summarizable_id (route_id or query_id)
|
||||||
|
|
||||||
|
# Universal metrics
|
||||||
|
t.integer :count, default: 0, null: false, comment: "Total number of requests/operations"
|
||||||
|
t.float :avg_duration, comment: "Average duration in milliseconds"
|
||||||
|
t.float :min_duration, comment: "Minimum duration in milliseconds"
|
||||||
|
t.float :max_duration, comment: "Maximum duration in milliseconds"
|
||||||
|
t.float :p50_duration, comment: "50th percentile duration"
|
||||||
|
t.float :p95_duration, comment: "95th percentile duration"
|
||||||
|
t.float :p99_duration, comment: "99th percentile duration"
|
||||||
|
t.float :total_duration, comment: "Total duration in milliseconds"
|
||||||
|
t.float :stddev_duration, comment: "Standard deviation of duration"
|
||||||
|
|
||||||
|
# Request/Route specific metrics
|
||||||
|
t.integer :error_count, default: 0, comment: "Number of error responses (5xx)"
|
||||||
|
t.integer :success_count, default: 0, comment: "Number of successful responses"
|
||||||
|
t.integer :status_2xx, default: 0, comment: "Number of 2xx responses"
|
||||||
|
t.integer :status_3xx, default: 0, comment: "Number of 3xx responses"
|
||||||
|
t.integer :status_4xx, default: 0, comment: "Number of 4xx responses"
|
||||||
|
t.integer :status_5xx, default: 0, comment: "Number of 5xx responses"
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
# Unique constraint and indexes for summaries
|
||||||
|
connection.add_index :rails_pulse_summaries, [ :summarizable_type, :summarizable_id, :period_type, :period_start ],
|
||||||
|
unique: true,
|
||||||
|
name: "idx_pulse_summaries_unique"
|
||||||
|
connection.add_index :rails_pulse_summaries, [ :period_type, :period_start ], name: "index_rails_pulse_summaries_on_period"
|
||||||
|
connection.add_index :rails_pulse_summaries, :created_at, name: "index_rails_pulse_summaries_on_created_at"
|
||||||
|
|
||||||
|
# Add indexes to existing tables for efficient aggregation
|
||||||
|
connection.add_index :rails_pulse_requests, [ :created_at, :route_id ], name: "idx_requests_for_aggregation"
|
||||||
|
connection.add_index :rails_pulse_requests, :created_at, name: "idx_requests_created_at"
|
||||||
|
|
||||||
|
connection.add_index :rails_pulse_operations, [ :created_at, :query_id ], name: "idx_operations_for_aggregation"
|
||||||
|
connection.add_index :rails_pulse_operations, :created_at, name: "idx_operations_created_at"
|
||||||
|
|
||||||
|
if ENV["CI"] == "true"
|
||||||
|
created_tables = required_tables.select { |table| connection.table_exists?(table) }
|
||||||
|
puts "[RailsPulse::Schema] Successfully created tables: #{created_tables.join(', ')}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if defined?(RailsPulse::ApplicationRecord)
|
||||||
|
RailsPulse::Schema.call(RailsPulse::ApplicationRecord.connection)
|
||||||
|
end
|
||||||
101
db/schema.rb
generated
101
db/schema.rb
generated
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.0].define(version: 2025_12_28_100000) do
|
ActiveRecord::Schema[8.0].define(version: 2025_12_28_163703) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pg_catalog.plpgsql"
|
enable_extension "pg_catalog.plpgsql"
|
||||||
enable_extension "postgis"
|
enable_extension "postgis"
|
||||||
|
|
@ -285,6 +285,102 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_28_100000) do
|
||||||
t.index ["user_id"], name: "index_points_raw_data_archives_on_user_id"
|
t.index ["user_id"], name: "index_points_raw_data_archives_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "rails_pulse_operations", force: :cascade do |t|
|
||||||
|
t.bigint "request_id", null: false, comment: "Link to the request"
|
||||||
|
t.bigint "query_id", comment: "Link to the normalized SQL query"
|
||||||
|
t.string "operation_type", null: false, comment: "Type of operation (e.g., database, view, gem_call)"
|
||||||
|
t.string "label", null: false, comment: "Descriptive name (e.g., SELECT FROM users WHERE id = 1, render layout)"
|
||||||
|
t.decimal "duration", precision: 15, scale: 6, null: false, comment: "Operation duration in milliseconds"
|
||||||
|
t.string "codebase_location", comment: "File and line number (e.g., app/models/user.rb:25)"
|
||||||
|
t.float "start_time", default: 0.0, null: false, comment: "Operation start time in milliseconds"
|
||||||
|
t.datetime "occurred_at", precision: nil, null: false, comment: "When the request started"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["created_at", "query_id"], name: "idx_operations_for_aggregation"
|
||||||
|
t.index ["created_at"], name: "idx_operations_created_at"
|
||||||
|
t.index ["occurred_at", "duration", "operation_type"], name: "index_rails_pulse_operations_on_time_duration_type"
|
||||||
|
t.index ["occurred_at"], name: "index_rails_pulse_operations_on_occurred_at"
|
||||||
|
t.index ["operation_type"], name: "index_rails_pulse_operations_on_operation_type"
|
||||||
|
t.index ["query_id", "duration", "occurred_at"], name: "index_rails_pulse_operations_query_performance"
|
||||||
|
t.index ["query_id", "occurred_at"], name: "index_rails_pulse_operations_on_query_and_time"
|
||||||
|
t.index ["query_id"], name: "index_rails_pulse_operations_on_query_id"
|
||||||
|
t.index ["request_id"], name: "index_rails_pulse_operations_on_request_id"
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table "rails_pulse_queries", force: :cascade do |t|
|
||||||
|
t.string "normalized_sql", limit: 1000, null: false, comment: "Normalized SQL query string (e.g., SELECT * FROM users WHERE id = ?)"
|
||||||
|
t.datetime "analyzed_at", comment: "When query analysis was last performed"
|
||||||
|
t.text "explain_plan", comment: "EXPLAIN output from actual SQL execution"
|
||||||
|
t.text "issues", comment: "JSON array of detected performance issues"
|
||||||
|
t.text "metadata", comment: "JSON object containing query complexity metrics"
|
||||||
|
t.text "query_stats", comment: "JSON object with query characteristics analysis"
|
||||||
|
t.text "backtrace_analysis", comment: "JSON object with call chain and N+1 detection"
|
||||||
|
t.text "index_recommendations", comment: "JSON array of database index recommendations"
|
||||||
|
t.text "n_plus_one_analysis", comment: "JSON object with enhanced N+1 query detection results"
|
||||||
|
t.text "suggestions", comment: "JSON array of optimization recommendations"
|
||||||
|
t.text "tags", comment: "JSON array of tags for filtering and categorization"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["normalized_sql"], name: "index_rails_pulse_queries_on_normalized_sql", unique: true
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table "rails_pulse_requests", force: :cascade do |t|
|
||||||
|
t.bigint "route_id", null: false, comment: "Link to the route"
|
||||||
|
t.decimal "duration", precision: 15, scale: 6, null: false, comment: "Total request duration in milliseconds"
|
||||||
|
t.integer "status", null: false, comment: "HTTP status code (e.g., 200, 500)"
|
||||||
|
t.boolean "is_error", default: false, null: false, comment: "True if status >= 500"
|
||||||
|
t.string "request_uuid", null: false, comment: "Unique identifier for the request (e.g., UUID)"
|
||||||
|
t.string "controller_action", comment: "Controller and action handling the request (e.g., PostsController#show)"
|
||||||
|
t.datetime "occurred_at", precision: nil, null: false, comment: "When the request started"
|
||||||
|
t.text "tags", comment: "JSON array of tags for filtering and categorization"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["created_at", "route_id"], name: "idx_requests_for_aggregation"
|
||||||
|
t.index ["created_at"], name: "idx_requests_created_at"
|
||||||
|
t.index ["occurred_at"], name: "index_rails_pulse_requests_on_occurred_at"
|
||||||
|
t.index ["request_uuid"], name: "index_rails_pulse_requests_on_request_uuid", unique: true
|
||||||
|
t.index ["route_id", "occurred_at"], name: "index_rails_pulse_requests_on_route_id_and_occurred_at"
|
||||||
|
t.index ["route_id"], name: "index_rails_pulse_requests_on_route_id"
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table "rails_pulse_routes", force: :cascade do |t|
|
||||||
|
t.string "method", null: false, comment: "HTTP method (e.g., GET, POST)"
|
||||||
|
t.string "path", null: false, comment: "Request path (e.g., /posts/index)"
|
||||||
|
t.text "tags", comment: "JSON array of tags for filtering and categorization"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["method", "path"], name: "index_rails_pulse_routes_on_method_and_path", unique: true
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table "rails_pulse_summaries", force: :cascade do |t|
|
||||||
|
t.datetime "period_start", null: false, comment: "Start of the aggregation period"
|
||||||
|
t.datetime "period_end", null: false, comment: "End of the aggregation period"
|
||||||
|
t.string "period_type", null: false, comment: "Aggregation period type: hour, day, week, month"
|
||||||
|
t.string "summarizable_type", null: false
|
||||||
|
t.bigint "summarizable_id", null: false, comment: "Link to Route or Query"
|
||||||
|
t.integer "count", default: 0, null: false, comment: "Total number of requests/operations"
|
||||||
|
t.float "avg_duration", comment: "Average duration in milliseconds"
|
||||||
|
t.float "min_duration", comment: "Minimum duration in milliseconds"
|
||||||
|
t.float "max_duration", comment: "Maximum duration in milliseconds"
|
||||||
|
t.float "p50_duration", comment: "50th percentile duration"
|
||||||
|
t.float "p95_duration", comment: "95th percentile duration"
|
||||||
|
t.float "p99_duration", comment: "99th percentile duration"
|
||||||
|
t.float "total_duration", comment: "Total duration in milliseconds"
|
||||||
|
t.float "stddev_duration", comment: "Standard deviation of duration"
|
||||||
|
t.integer "error_count", default: 0, comment: "Number of error responses (5xx)"
|
||||||
|
t.integer "success_count", default: 0, comment: "Number of successful responses"
|
||||||
|
t.integer "status_2xx", default: 0, comment: "Number of 2xx responses"
|
||||||
|
t.integer "status_3xx", default: 0, comment: "Number of 3xx responses"
|
||||||
|
t.integer "status_4xx", default: 0, comment: "Number of 4xx responses"
|
||||||
|
t.integer "status_5xx", default: 0, comment: "Number of 5xx responses"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["created_at"], name: "index_rails_pulse_summaries_on_created_at"
|
||||||
|
t.index ["period_type", "period_start"], name: "index_rails_pulse_summaries_on_period"
|
||||||
|
t.index ["summarizable_type", "summarizable_id", "period_type", "period_start"], name: "idx_pulse_summaries_unique", unique: true
|
||||||
|
t.index ["summarizable_type", "summarizable_id"], name: "index_rails_pulse_summaries_on_summarizable"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "stats", force: :cascade do |t|
|
create_table "stats", force: :cascade do |t|
|
||||||
t.integer "year", null: false
|
t.integer "year", null: false
|
||||||
t.integer "month", null: false
|
t.integer "month", null: false
|
||||||
|
|
@ -429,6 +525,9 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_28_100000) do
|
||||||
add_foreign_key "points", "users"
|
add_foreign_key "points", "users"
|
||||||
add_foreign_key "points", "visits"
|
add_foreign_key "points", "visits"
|
||||||
add_foreign_key "points_raw_data_archives", "users"
|
add_foreign_key "points_raw_data_archives", "users"
|
||||||
|
add_foreign_key "rails_pulse_operations", "rails_pulse_queries", column: "query_id"
|
||||||
|
add_foreign_key "rails_pulse_operations", "rails_pulse_requests", column: "request_id"
|
||||||
|
add_foreign_key "rails_pulse_requests", "rails_pulse_routes", column: "route_id"
|
||||||
add_foreign_key "stats", "users"
|
add_foreign_key "stats", "users"
|
||||||
add_foreign_key "taggings", "tags"
|
add_foreign_key "taggings", "tags"
|
||||||
add_foreign_key "tags", "users"
|
add_foreign_key "tags", "users"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue