Add PgHero (#894)

This commit is contained in:
Mahad Kalam 2026-02-02 21:17:26 +00:00 committed by GitHub
parent 797af036e9
commit a2716fcdb0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 214 additions and 3 deletions

View file

@ -161,3 +161,6 @@ gem "autotuner", "~> 1.0"
gem "tailwindcss-ruby", "~> 4.1"
gem "tailwindcss-rails", "~> 4.2"
gem "pghero"
gem "pg_query", ">= 2"

View file

@ -228,6 +228,24 @@ GEM
fugit (>= 1.11.0)
railties (>= 6.1.0)
thor (>= 1.0.0)
google-protobuf (4.33.4)
bigdecimal
rake (>= 13)
google-protobuf (4.33.4-aarch64-linux-gnu)
bigdecimal
rake (>= 13)
google-protobuf (4.33.4-aarch64-linux-musl)
bigdecimal
rake (>= 13)
google-protobuf (4.33.4-arm64-darwin)
bigdecimal
rake (>= 13)
google-protobuf (4.33.4-x86_64-linux-gnu)
bigdecimal
rake (>= 13)
google-protobuf (4.33.4-x86_64-linux-musl)
bigdecimal
rake (>= 13)
groupdate (6.7.0)
activesupport (>= 7.1)
hashie (5.1.0)
@ -358,6 +376,10 @@ GEM
pg (1.6.3-arm64-darwin)
pg (1.6.3-x86_64-linux)
pg (1.6.3-x86_64-linux-musl)
pg_query (6.2.2)
google-protobuf (>= 3.25.3)
pghero (3.7.0)
activerecord (>= 7.1)
pp (0.6.3)
prettyprint
prettyprint (0.2.0)
@ -655,6 +677,8 @@ DEPENDENCIES
pagy (~> 43.2)
paper_trail
pg
pg_query (>= 2)
pghero
propshaft
public_activity
puma (>= 5.0)

View file

@ -0,0 +1,7 @@
class Pghero::CaptureQueryStatsJob < ApplicationJob
queue_as :literally_whenever
def perform
PgHero.capture_query_stats
end
end

View file

@ -0,0 +1,7 @@
class Pghero::CaptureSpaceStatsJob < ApplicationJob
queue_as :literally_whenever
def perform
PgHero.capture_space_stats
end
end

View file

@ -0,0 +1,10 @@
class Pghero::CleanStatsJob < ApplicationJob
queue_as :literally_whenever
KEEP_DAYS = 30
def perform
PgHero.clean_query_stats(before: KEEP_DAYS.days.ago)
PgHero.clean_space_stats(before: KEEP_DAYS.days.ago)
end
end

View file

@ -171,6 +171,11 @@
Feature Flags
<% end %>
<% end %>
<% superadmin_tool(nil, "div") do %>
<%= link_to pghero_path, class: "block px-2 py-1 rounded-lg transition #{current_page?(pghero_path) ? 'bg-primary text-primary' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
PgHero
<% end %>
<% end %>
<%= render_activities(@activities) if defined?(@activities) %>
</div>

View file

@ -123,6 +123,23 @@ Rails.application.configure do
cron: "0 2 * * *",
class: "ProcessAccountDeletionsJob",
description: "nuke accounts after 30 days"
},
# PgHero stats
pghero_capture_query_stats: {
cron: "*/5 * * * *",
class: "Pghero::CaptureQueryStatsJob",
description: "Capture query stats for PgHero historical tracking"
},
pghero_capture_space_stats: {
cron: "0 4 * * *",
class: "Pghero::CaptureSpaceStatsJob",
description: "Capture space stats for PgHero historical tracking (daily)"
},
pghero_clean_stats: {
cron: "0 5 * * *",
class: "Pghero::CleanStatsJob",
description: "Clean PgHero stats older than 30 days"
}
# sync_stale_repo_metadata: {
# cron: "0 4 * * *", # Daily at 4 AM

View file

@ -23,6 +23,7 @@ Rails.application.routes.draw do
mount GoodJob::Engine => "good_job"
mount AhoyCaptain::Engine => "/ahoy_captain"
mount Flipper::UI.app(Flipper) => "flipper", as: :flipper
mount PgHero::Engine => "pghero"
namespace :admin do
resources :admin_users, only: [ :index, :update ] do

View file

@ -0,0 +1,15 @@
class CreatePgheroQueryStats < ActiveRecord::Migration[8.1]
def change
create_table :pghero_query_stats do |t|
t.text :database
t.text :user
t.text :query
t.integer :query_hash, limit: 8
t.float :total_time
t.integer :calls, limit: 8
t.timestamp :captured_at
end
add_index :pghero_query_stats, [:database, :captured_at]
end
end

View file

@ -0,0 +1,13 @@
class CreatePgheroSpaceStats < ActiveRecord::Migration[8.1]
def change
create_table :pghero_space_stats do |t|
t.text :database
t.text :schema
t.text :relation
t.integer :size, limit: 8
t.timestamp :captured_at
end
add_index :pghero_space_stats, [:database, :captured_at]
end
end

115
db/schema.rb generated
View file

@ -10,8 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.1].define(version: 2026_01_20_014910) do
create_schema "pganalyze"
ActiveRecord::Schema[8.1].define(version: 2026_02_02_210555) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"
enable_extension "pg_stat_statements"
@ -260,49 +259,128 @@ ActiveRecord::Schema[8.1].define(version: 2026_01_20_014910) do
t.index ["scheduled_at"], name: "index_good_jobs_on_scheduled_at", where: "(finished_at IS NULL)"
end
create_table "heartbeat_branches", force: :cascade do |t|
t.datetime "created_at", null: false
t.string "name", null: false
t.datetime "updated_at", null: false
t.bigint "user_id", null: false
t.index ["user_id", "name"], name: "index_heartbeat_branches_on_user_id_and_name", unique: true
t.index ["user_id"], name: "index_heartbeat_branches_on_user_id"
end
create_table "heartbeat_categories", force: :cascade do |t|
t.datetime "created_at", null: false
t.string "name", null: false
t.datetime "updated_at", null: false
t.index ["name"], name: "index_heartbeat_categories_on_name", unique: true
end
create_table "heartbeat_editors", force: :cascade do |t|
t.datetime "created_at", null: false
t.string "name", null: false
t.datetime "updated_at", null: false
t.index ["name"], name: "index_heartbeat_editors_on_name", unique: true
end
create_table "heartbeat_languages", force: :cascade do |t|
t.datetime "created_at", null: false
t.string "name", null: false
t.datetime "updated_at", null: false
t.index ["name"], name: "index_heartbeat_languages_on_name", unique: true
end
create_table "heartbeat_machines", force: :cascade do |t|
t.datetime "created_at", null: false
t.string "name", null: false
t.datetime "updated_at", null: false
t.bigint "user_id", null: false
t.index ["user_id", "name"], name: "index_heartbeat_machines_on_user_id_and_name", unique: true
t.index ["user_id"], name: "index_heartbeat_machines_on_user_id"
end
create_table "heartbeat_operating_systems", force: :cascade do |t|
t.datetime "created_at", null: false
t.string "name", null: false
t.datetime "updated_at", null: false
t.index ["name"], name: "index_heartbeat_operating_systems_on_name", unique: true
end
create_table "heartbeat_projects", force: :cascade do |t|
t.datetime "created_at", null: false
t.string "name", null: false
t.datetime "updated_at", null: false
t.bigint "user_id", null: false
t.index ["user_id", "name"], name: "index_heartbeat_projects_on_user_id_and_name", unique: true
t.index ["user_id"], name: "index_heartbeat_projects_on_user_id"
end
create_table "heartbeat_user_agents", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "value", null: false
t.index ["value"], name: "index_heartbeat_user_agents_on_value", unique: true
end
create_table "heartbeats", force: :cascade do |t|
t.string "branch"
t.bigint "branch_id"
t.string "category"
t.bigint "category_id"
t.datetime "created_at", null: false
t.integer "cursorpos"
t.datetime "deleted_at"
t.string "dependencies", default: [], array: true
t.string "editor"
t.bigint "editor_id"
t.string "entity"
t.text "fields_hash"
t.inet "ip_address"
t.boolean "is_write"
t.string "language"
t.bigint "language_id"
t.integer "line_additions"
t.integer "line_deletions"
t.integer "lineno"
t.integer "lines"
t.string "machine"
t.bigint "machine_id"
t.string "operating_system"
t.bigint "operating_system_id"
t.string "project"
t.bigint "project_id"
t.integer "project_root_count"
t.jsonb "raw_data"
t.bigint "raw_heartbeat_upload_id"
t.integer "source_type", null: false
t.float "time", null: false
t.string "type"
t.datetime "updated_at", null: false
t.string "user_agent"
t.bigint "user_agent_id"
t.bigint "user_id", null: false
t.integer "ysws_program", default: 0, null: false
t.index ["branch_id"], name: "index_heartbeats_on_branch_id"
t.index ["category", "time"], name: "index_heartbeats_on_category_and_time"
t.index ["category_id"], name: "index_heartbeats_on_category_id"
t.index ["editor_id"], name: "index_heartbeats_on_editor_id"
t.index ["fields_hash"], name: "index_heartbeats_on_fields_hash_when_not_deleted", unique: true, where: "(deleted_at IS NULL)"
t.index ["ip_address"], name: "index_heartbeats_on_ip_address"
t.index ["language_id"], name: "index_heartbeats_on_language_id"
t.index ["machine"], name: "index_heartbeats_on_machine"
t.index ["machine_id"], name: "index_heartbeats_on_machine_id"
t.index ["operating_system_id"], name: "index_heartbeats_on_operating_system_id"
t.index ["project", "time"], name: "index_heartbeats_on_project_and_time"
t.index ["project"], name: "index_heartbeats_on_project"
t.index ["project_id"], name: "index_heartbeats_on_project_id"
t.index ["raw_heartbeat_upload_id"], name: "index_heartbeats_on_raw_heartbeat_upload_id"
t.index ["source_type", "time", "user_id", "project"], name: "index_heartbeats_on_source_type_time_user_project"
t.index ["user_agent_id"], name: "index_heartbeats_on_user_agent_id"
t.index ["user_id", "id"], name: "index_heartbeats_on_user_id_with_ip", order: { id: :desc }, where: "((ip_address IS NOT NULL) AND (deleted_at IS NULL))"
t.index ["user_id", "project", "time"], name: "idx_heartbeats_user_project_time_stats", where: "((deleted_at IS NULL) AND (project IS NOT NULL))"
t.index ["user_id", "time", "category"], name: "index_heartbeats_on_user_time_category"
t.index ["user_id", "time", "language"], name: "idx_heartbeats_user_time_language_stats", where: "(deleted_at IS NULL)"
t.index ["user_id", "time", "language_id"], name: "idx_heartbeats_user_time_language_id", where: "(deleted_at IS NULL)"
t.index ["user_id", "time", "project"], name: "idx_heartbeats_user_time_project_stats", where: "(deleted_at IS NULL)"
t.index ["user_id", "time", "project_id"], name: "idx_heartbeats_user_time_project_id", where: "(deleted_at IS NULL)"
t.index ["user_id", "time"], name: "idx_heartbeats_user_time_active", where: "(deleted_at IS NULL)"
t.index ["user_id"], name: "index_heartbeats_on_user_id"
end
@ -378,6 +456,26 @@ ActiveRecord::Schema[8.1].define(version: 2026_01_20_014910) do
t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true
end
create_table "pghero_query_stats", force: :cascade do |t|
t.bigint "calls"
t.datetime "captured_at", precision: nil
t.text "database"
t.text "query"
t.bigint "query_hash"
t.float "total_time"
t.text "user"
t.index ["database", "captured_at"], name: "index_pghero_query_stats_on_database_and_captured_at"
end
create_table "pghero_space_stats", force: :cascade do |t|
t.datetime "captured_at", precision: nil
t.text "database"
t.text "relation"
t.text "schema"
t.bigint "size"
t.index ["database", "captured_at"], name: "index_pghero_space_stats_on_database_and_captured_at"
end
create_table "project_labels", force: :cascade do |t|
t.datetime "created_at", null: false
t.string "label"
@ -578,6 +676,17 @@ ActiveRecord::Schema[8.1].define(version: 2026_01_20_014910) do
add_foreign_key "deletion_requests", "users", column: "admin_approved_by_id"
add_foreign_key "email_addresses", "users"
add_foreign_key "email_verification_requests", "users"
add_foreign_key "heartbeat_branches", "users"
add_foreign_key "heartbeat_machines", "users"
add_foreign_key "heartbeat_projects", "users"
add_foreign_key "heartbeats", "heartbeat_branches", column: "branch_id"
add_foreign_key "heartbeats", "heartbeat_categories", column: "category_id"
add_foreign_key "heartbeats", "heartbeat_editors", column: "editor_id"
add_foreign_key "heartbeats", "heartbeat_languages", column: "language_id"
add_foreign_key "heartbeats", "heartbeat_machines", column: "machine_id"
add_foreign_key "heartbeats", "heartbeat_operating_systems", column: "operating_system_id"
add_foreign_key "heartbeats", "heartbeat_projects", column: "project_id"
add_foreign_key "heartbeats", "heartbeat_user_agents", column: "user_agent_id"
add_foreign_key "heartbeats", "raw_heartbeat_uploads"
add_foreign_key "heartbeats", "users"
add_foreign_key "leaderboard_entries", "leaderboards"