pg_reports 0.8.2 → 0.9.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.
@@ -556,7 +556,7 @@ uk:
556
556
  page_title: "PgReports — Дашборд"
557
557
  navigation:
558
558
  dashboard: "Дашборд"
559
- back: "Назад"
559
+ back: "Назад"
560
560
  database_selector:
561
561
  label: "База даних"
562
562
  target_selector:
@@ -564,40 +564,41 @@ uk:
564
564
  actions:
565
565
  cancel: "Скасувати"
566
566
  retry: "Повторити"
567
- copy: "📋 Копіювати"
568
- copy_query: "📋 Копіювати запит"
569
- copy_code: "📋 Копіювати код"
567
+ copy: "Копіювати"
568
+ copy_query: "Копіювати запит"
569
+ copy_code: "Копіювати код"
570
570
  copy_to_clipboard_title: "Скопіювати в буфер обміну"
571
- copied_feedback: "Скопійовано!"
571
+ copied_feedback: "Скопійовано!"
572
572
  clear_all: "Очистити все"
573
- run_report: "Запустити звіт"
574
- export: "Експорт"
575
- download_text: "📄 Текст (.txt)"
576
- download_csv: "📊 CSV (.csv)"
577
- download_json: "📋 JSON (.json)"
578
- download: "📥 Завантажити"
573
+ run_report: "Запустити звіт"
574
+ export: "Експорт"
575
+ download_text: "Текст (.txt)"
576
+ download_csv: "CSV (.csv)"
577
+ download_json: "JSON (.json)"
578
+ download: "Завантажити"
579
579
  copy_ai_prompt: "Копіювати промпт"
580
- send_telegram: "📨 Telegram"
580
+ send_telegram: "Telegram"
581
581
  sending: "Надсилання..."
582
- reset_statistics: "🗑️ Скинути статистику"
582
+ reset_statistics: "Скинути статистику"
583
583
  resetting: "Скидання..."
584
584
  confirm_reset: "Так, скинути"
585
- create_extension: "Створити розширення"
585
+ create_extension: "Створити розширення"
586
586
  creating: "Створення..."
587
587
  ide_settings_button_title: "Налаштування IDE"
588
- explain_analyze: "📊 EXPLAIN ANALYZE"
589
- execute_query: "Виконати запит"
588
+ explain_analyze: "EXPLAIN ANALYZE"
589
+ execute_query: "Виконати запит"
590
590
  run_query: "SQL-консоль"
591
- create_migration_file: "📁 Створити файл і відкрити в IDE"
592
- start_monitoring: "Запустити моніторинг"
593
- stop_monitoring: "Зупинити моніторинг"
591
+ create_migration_file: "Створити файл і відкрити в IDE"
592
+ start_monitoring: "Запустити моніторинг"
593
+ stop_monitoring: "Зупинити моніторинг"
594
594
  starting: "Запуск..."
595
595
  stopping: "Зупинка..."
596
- load_history: "📜 Завантажити історію (50)"
596
+ load_history: "Завантажити історію"
597
+ load_history_unavailable_title: "Збереженої історії немає — задайте config.query_monitor_log_file, щоб запити зберігалися між перезапусками"
597
598
  loading: "Завантаження..."
598
599
  running: "Виконання..."
599
- save_for_comparison: "📌 Зберегти для порівняння"
600
- saved_marker: "📌 Збережено"
600
+ save_for_comparison: "Зберегти для порівняння"
601
+ saved_marker: "Збережено"
601
602
  status:
602
603
  pg_stat_ready: "Активно"
603
604
  not_preloaded: "Потрібне передзавантаження"
@@ -615,19 +616,20 @@ uk:
615
616
  create_extension_title: "Створення розширення pg_stat_statements"
616
617
  create_extension_intro: "Розширення pg_stat_statements ще не створено в цій базі даних. Натисніть, щоб створити його:"
617
618
  create_extension_note: "Якщо бібліотеку не передзавантажено, її також потрібно додати до shared_preload_libraries та перезапустити PostgreSQL."
618
- reset_stats_title: "⚠️ Скидання статистики"
619
+ reset_stats_title: "Скидання статистики"
619
620
  reset_stats_confirm: "Ви впевнені, що хочете скинути статистику pg_stat_statements?"
620
621
  reset_stats_warning: "Ця дія очистить усю зібрану статистику запитів і не може бути скасована."
621
- ide_settings_title: "⚙️ Налаштування IDE"
622
- problem_detected_title: "⚠️ Виявлено проблему"
623
- query_analyzer_title: "📊 Аналізатор запиту"
622
+ ide_settings_title: "Налаштування IDE"
623
+ problem_detected_title: "Виявлено проблему"
624
+ query_analyzer_title: "Аналізатор запиту"
624
625
  query_label: "Запит:"
625
626
  parameters_label: "Параметри:"
626
- migration_title: "🗑️ Міграція видалення індексу"
627
+ migration_title: "Міграція видалення індексу"
627
628
  migration_subtitle: "Згенерована міграція для видалення індексу:"
628
629
  migration_warning: "Створення міграції згенерує файл міграції у вашому проєкті. Запуск цієї міграції видалить індекс із БД, що може значно вплинути на продуктивність застосунку."
629
630
  migration_warning_dev_only: "Цю операцію слід виконувати лише в локальному dev-середовищі."
630
- query_execution_disabled_title: "⚠️ Виконання запитів вимкнено"
631
+ query_execution_disabled_title: "Виконання запитів вимкнено"
632
+ migration_disabled_title: "Створення міграцій вимкнено"
631
633
  query_execution_disabled_intro: "Щоб увімкнути цю функцію, додайте до конфігурації:"
632
634
  query_execution_disabled_env_note: "...або замість цього встановіть змінну середовища:"
633
635
  run_query_title: "SQL-консоль"
@@ -670,25 +672,27 @@ uk:
670
672
  waiting_for_locks: "чекають блокувань"
671
673
  percent_used_suffix: "% використано"
672
674
  categories:
673
- requires_pg_stat: "🔒 Потрібен pg_stat_statements"
675
+ requires_pg_stat: "Потрібен pg_stat_statements"
676
+ pg_stat_missing_reason: "Потрібен pg_stat_statements, але розширення не встановлено в базі «%{database}». Створіть його там або перемкніться на базу, де воно є."
677
+ pg_stat_not_preloaded_reason: "Потрібен pg_stat_statements. Розширення є в базі «%{database}», але відсутнє в shared_preload_libraries — даних не буде, доки PostgreSQL не перезапустять із ним."
674
678
  reports_count_suffix: "звітів"
675
- primary_only_reason: "🔒 Ця категорія працює лише на первинній базі — вона перевіряє моделі хост-застосунку. Щоб користуватися, перемкніться на базу за замовчуванням."
676
- standalone_no_host_app_reason: "🔒 Недоступно в автономному режимі — ця категорія перевіряє моделі хост-застосунку, а при самостійному запуску дашборда хост-застосунку немає."
679
+ primary_only_reason: "Ця категорія працює лише на первинній базі — вона перевіряє моделі хост-застосунку. Щоб користуватися, перемкніться на базу за замовчуванням."
680
+ standalone_no_host_app_reason: "Недоступно в автономному режимі — ця категорія перевіряє моделі хост-застосунку, а при самостійному запуску дашборда хост-застосунку немає."
677
681
  documentation:
678
- toggle_title: "📖 Що показує цей звіт?"
679
- what_section: "📋 Що"
680
- why_section: "Чому це важливо"
681
- nuances_section: "⚠️ Нюанси"
682
- thresholds_section: "📊 Пороги"
683
- threshold_warning_label: "⚠️ Warning:"
684
- threshold_critical_label: "🔴 Critical:"
682
+ toggle_title: "Що показує цей звіт?"
683
+ what_section: "Що"
684
+ why_section: "Чому це важливо"
685
+ nuances_section: "Нюанси"
686
+ thresholds_section: "Пороги"
687
+ threshold_warning_label: "Warning:"
688
+ threshold_critical_label: "Critical:"
685
689
  threshold_inverted_note: "(менше — гірше)"
686
690
  filters:
687
- title: "🔍 Параметри фільтрації"
691
+ title: "Параметри фільтрації"
688
692
  current_value: "зараз"
689
693
  saved:
690
- title: "📌 Збережено для порівняння"
691
- saved_at_prefix: "Збережено:"
694
+ title: "Збережено для порівняння"
695
+ saved_at_prefix: "Збережено:"
692
696
  click_to_expand: "Натисніть, щоб розгорнути"
693
697
  confirm_clear_all: "Видалити всі збережені записи для цього звіту?"
694
698
  remove_title: "Видалити"
@@ -702,9 +706,9 @@ uk:
702
706
  execution_time_label: "Час виконання:"
703
707
  null_placeholder: "<null>"
704
708
  sections:
705
- recommendation: "💡 Рекомендація"
706
- detected_issues: "⚠️ Виявлені проблеми"
707
- execution_plan: "📊 План виконання"
709
+ recommendation: "Рекомендація"
710
+ detected_issues: "Виявлені проблеми"
711
+ execution_plan: "План виконання"
708
712
  line_label: "Рядок"
709
713
  current_label: "Поточне:"
710
714
  threshold_label: "Поріг:"
@@ -712,8 +716,8 @@ uk:
712
716
  warning_eq: "warning"
713
717
  critical_eq: "critical"
714
718
  levels:
715
- critical: "🔴 Критично"
716
- warning: "⚠️ Увага"
719
+ critical: "Критично"
720
+ warning: "Увага"
717
721
  errors:
718
722
  error_prefix: "Помилка:"
719
723
  unable_fetch_metrics: "Не вдалося отримати статистику БД."
@@ -741,8 +745,9 @@ uk:
741
745
  run_query_failed: "Не вдалося виконати запит"
742
746
  query_required: "Введіть запит"
743
747
  create_migration_failed: "Не вдалося створити міграцію"
744
- explain_disabled_toast: "⚠️ EXPLAIN ANALYZE вимкнено. Увімкніть у конфігурації: config.allow_raw_query_execution = true"
745
- execute_disabled_toast: "⚠️ Виконання запитів вимкнено. Увімкніть у конфігурації: config.allow_raw_query_execution = true"
748
+ explain_disabled_toast: "EXPLAIN ANALYZE вимкнено. Увімкніть у конфігурації: config.allow_raw_query_execution = true"
749
+ execute_disabled_toast: "Виконання запитів вимкнено. Увімкніть у конфігурації: config.allow_raw_query_execution = true"
750
+ migration_disabled_toast: "Створення міграцій вимкнено. Увімкніть у конфігурації: config.allow_raw_query_execution = true"
746
751
  query_monitoring_error: "Помилка моніторингу запитів"
747
752
  query_hash_required: "Потрібен хеш запиту"
748
753
  query_execution_disabled: "Виконання запитів із дашборду вимкнено. Увімкніть у конфігурації: 'config.allow_raw_query_execution = true'"
@@ -9,6 +9,16 @@ module PgReports
9
9
  # PgReports::Connection::ErrorTranslator.translate(error)
10
10
  # # => { title: "...", detail: "...", hint: "GRANT ...", code: "42501" }
11
11
  module ErrorTranslator
12
+ # Relations the reports read that are supplied by an extension rather than
13
+ # by core PostgreSQL. Used to turn "relation X does not exist" into the
14
+ # actionable "extension Y isn't installed here".
15
+ EXTENSION_RELATIONS = {
16
+ "pg_stat_statements" => "pg_stat_statements",
17
+ "pg_stat_statements_info" => "pg_stat_statements",
18
+ "pgstattuple" => "pgstattuple",
19
+ "pg_buffercache" => "pg_buffercache"
20
+ }.freeze
21
+
12
22
  module_function
13
23
 
14
24
  # Returns a Hash with :title, :detail, :hint, :code, :raw_message.
@@ -23,6 +33,8 @@ module PgReports
23
33
  when "28000", "28P01" then auth_failed(message)
24
34
  when "08001", "08006", "08000", "08003", "08004" then connection_refused(message)
25
35
  when "53300" then too_many_connections(message)
36
+ when "42P01" then undefined_table(message)
37
+ when "42883" then undefined_function(message)
26
38
  else generic(error)
27
39
  end
28
40
 
@@ -92,6 +104,37 @@ module PgReports
92
104
  }
93
105
  end
94
106
 
107
+ # A report asked for a relation this database doesn't have. Almost always an
108
+ # extension that exists on one database in the cluster but not the one
109
+ # currently selected — extensions are per-database, the dashboard is not.
110
+ def undefined_table(message)
111
+ target = extract_object(message, /relation "([^"]+)" does not exist/)
112
+
113
+ if target && EXTENSION_RELATIONS.key?(target)
114
+ extension = EXTENSION_RELATIONS.fetch(target)
115
+ return {
116
+ title: "#{extension} not available",
117
+ detail: "This report reads \"#{target}\", which does not exist on the selected database.",
118
+ hint: "CREATE EXTENSION IF NOT EXISTS #{extension};"
119
+ }
120
+ end
121
+
122
+ {
123
+ title: "Relation not found",
124
+ detail: target ? "The selected database has no relation named \"#{target}\"." : "The report referenced a relation that does not exist on the selected database.",
125
+ hint: nil
126
+ }
127
+ end
128
+
129
+ def undefined_function(message)
130
+ target = extract_object(message, /function ([\w.]+)\(/)
131
+ {
132
+ title: "Function not found",
133
+ detail: target ? "The selected database has no function named \"#{target}\"." : "The report called a function that does not exist on the selected database.",
134
+ hint: "It is usually provided by an extension that is not installed on this database."
135
+ }
136
+ end
137
+
95
138
  def generic(error)
96
139
  {
97
140
  title: error.class.name.split("::").last,
@@ -237,6 +237,10 @@ module PgReports
237
237
  name: "Queries",
238
238
  icon: "⚡",
239
239
  color: "#6366f1",
240
+ # Every report here reads the pg_stat_statements view. The extension is
241
+ # created per-database, so availability has to be re-checked against
242
+ # whichever database the dashboard is pointed at — not once at boot.
243
+ requires: :pg_stat_statements,
240
244
  reports: {
241
245
  slow_queries: {name: "Slow Queries", description: "Queries with high mean execution time"},
242
246
  heavy_queries: {name: "Heavy Queries", description: "Most frequently called queries"},
@@ -412,6 +416,12 @@ module PgReports
412
416
  def self.target_constraint(category)
413
417
  REPORTS.dig(category.to_sym, :target_constraint)
414
418
  end
419
+
420
+ # A database-level dependency the whole category needs, or nil. Currently
421
+ # only :pg_stat_statements — the caller resolves what "available" means.
422
+ def self.requires(category)
423
+ REPORTS.dig(category.to_sym, :requires)
424
+ end
415
425
  end
416
426
  end
417
427
  end
@@ -313,15 +313,12 @@ module PgReports
313
313
  if @summary[:critical_problems] > 0
314
314
  @summary[:status] = "critical"
315
315
  @summary[:status_text] = "Critical issues detected"
316
- @summary[:status_icon] = "🔴"
317
316
  elsif @summary[:warnings] > 0
318
317
  @summary[:status] = "warning"
319
318
  @summary[:status_text] = "Potential issues detected"
320
- @summary[:status_icon] = "🟡"
321
319
  else
322
320
  @summary[:status] = "good"
323
321
  @summary[:status_text] = "No issues detected"
324
- @summary[:status_icon] = "🟢"
325
322
  end
326
323
 
327
324
  # Group problems by type for summary
@@ -106,10 +106,18 @@ module PgReports
106
106
  {
107
107
  enabled: enabled,
108
108
  session_id: session_id,
109
- query_count: @queries.size
109
+ query_count: @queries.size,
110
+ history_available: history_available?
110
111
  }
111
112
  end
112
113
 
114
+ # Whether #load_from_log has anything to return. History is persisted only
115
+ # when a log file is configured, and the file exists only once something has
116
+ # been written to it — without both, "Load history" can only ever no-op.
117
+ def history_available?
118
+ log_file_enabled? && File.exist?(log_file_path)
119
+ end
120
+
113
121
  def queries(limit: nil, session_id: nil)
114
122
  result = @queries.dup
115
123
 
@@ -348,6 +356,10 @@ module PgReports
348
356
  end
349
357
  end
350
358
 
359
+ # JSON.generate, not #to_json: ActiveSupport overrides #to_json and (up to
360
+ # Rails 7.0) passes json's removed `quirks_mode` option, so on json >= 3 it
361
+ # raises ArgumentError. The rescue below would swallow that and leave the
362
+ # log silently empty. json's own generator takes no such option.
351
363
  def write_session_marker(marker_type, sid = @session_id)
352
364
  return unless log_file_enabled?
353
365
 
@@ -358,7 +370,7 @@ module PgReports
358
370
  }
359
371
 
360
372
  File.open(log_file_path, "a") do |f|
361
- f.puts marker.to_json
373
+ f.puts JSON.generate(marker)
362
374
  end
363
375
  rescue => e
364
376
  # Silently fail - don't break monitoring if file write fails
@@ -371,7 +383,7 @@ module PgReports
371
383
 
372
384
  File.open(log_file_path, "a") do |f|
373
385
  @queries.each do |query|
374
- f.puts query.to_json
386
+ f.puts JSON.generate(query)
375
387
  end
376
388
  end
377
389
  rescue => e
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgReports
4
- VERSION = "0.8.2"
4
+ VERSION = "0.9.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_reports
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.2
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eldar Avatov