Module: PgReports::Connection::ErrorTranslator
- Defined in:
- lib/pg_reports/connection/error_translator.rb
Overview
Translates raw PG / ActiveRecord exceptions into human-readable messages with concrete remediation hints (typically a GRANT statement).
Usage:
PgReports::Connection::ErrorTranslator.translate(error)
# => { title: "...", detail: "...", hint: "GRANT ...", code: "42501" }
Constant Summary collapse
- EXTENSION_RELATIONS =
Relations the reports read that are supplied by an extension rather than by core PostgreSQL. Used to turn "relation X does not exist" into the actionable "extension Y isn't installed here".
{ "pg_stat_statements" => "pg_stat_statements", "pg_stat_statements_info" => "pg_stat_statements", "pgstattuple" => "pgstattuple", "pg_buffercache" => "pg_buffercache" }.freeze
Class Method Summary collapse
- .auth_failed(_message) ⇒ Object
- .connection_refused(_message) ⇒ Object
- .database_does_not_exist(message) ⇒ Object
- .extract_object(message, regex) ⇒ Object
- .generic(error) ⇒ Object
- .permission_denied(message) ⇒ Object
- .sqlstate_for(error) ⇒ Object
- .too_many_connections(_message) ⇒ Object
-
.translate(error) ⇒ Object
Returns a Hash with :title, :detail, :hint, :code, :raw_message.
- .undefined_function(message) ⇒ Object
-
.undefined_table(message) ⇒ Object
A report asked for a relation this database doesn't have.
Class Method Details
.auth_failed(_message) ⇒ Object
83 84 85 86 87 88 89 |
# File 'lib/pg_reports/connection/error_translator.rb', line 83 def auth_failed() { title: "Authentication failed", detail: "PostgreSQL rejected the credentials for this target.", hint: "Verify the username/password in the target configuration; check pg_hba.conf for the connecting host." } end |
.connection_refused(_message) ⇒ Object
91 92 93 94 95 96 97 |
# File 'lib/pg_reports/connection/error_translator.rb', line 91 def connection_refused() { title: "Cannot reach PostgreSQL", detail: "The server is unreachable or refused the connection.", hint: "Check host/port, network reachability, and that PostgreSQL is accepting connections." } end |
.database_does_not_exist(message) ⇒ Object
74 75 76 77 78 79 80 81 |
# File 'lib/pg_reports/connection/error_translator.rb', line 74 def database_does_not_exist() target = extract_object(, /database "([^"]+)" does not exist/) { title: "Database not found", detail: target ? "PostgreSQL has no database named \"#{target}\"." : "The requested database does not exist on this server.", hint: nil } end |
.extract_object(message, regex) ⇒ Object
146 147 148 149 |
# File 'lib/pg_reports/connection/error_translator.rb', line 146 def extract_object(, regex) match = .match(regex) match && match[1] end |
.generic(error) ⇒ Object
138 139 140 141 142 143 144 |
# File 'lib/pg_reports/connection/error_translator.rb', line 138 def generic(error) { title: error.class.name.split("::").last, detail: error., hint: nil } end |
.permission_denied(message) ⇒ Object
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# File 'lib/pg_reports/connection/error_translator.rb', line 55 def () target = extract_object(, /permission denied for (?:database|schema|table|relation|view) "?([\w.]+)"?/) kind = extract_object(, /permission denied for (database|schema|table|relation|view)/) hint = if kind && target case kind when "database" then "GRANT CONNECT ON DATABASE #{target} TO <role>;" when "schema" then "GRANT USAGE ON SCHEMA #{target} TO <role>;" when "table", "relation", "view" then "GRANT SELECT ON #{target} TO <role>;" end end { title: "Permission denied", detail: (kind && target) ? "The connecting role does not have the required privilege on #{kind} \"#{target}\"." : "The connecting role lacks the required privilege.", hint: hint } end |
.sqlstate_for(error) ⇒ Object
44 45 46 47 48 49 50 51 52 53 |
# File 'lib/pg_reports/connection/error_translator.rb', line 44 def sqlstate_for(error) case error when PG::Error error.result&.error_field(PG::Result::PG_DIAG_SQLSTATE) when ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotEstablished sqlstate_for(error.cause) if error.cause && !error.cause.equal?(error) end rescue nil end |
.too_many_connections(_message) ⇒ Object
99 100 101 102 103 104 105 |
# File 'lib/pg_reports/connection/error_translator.rb', line 99 def too_many_connections() { title: "Too many connections", detail: "PostgreSQL refused the connection because max_connections is reached.", hint: "Wait, increase max_connections, or use a connection pooler (PgBouncer)." } end |
.translate(error) ⇒ Object
Returns a Hash with :title, :detail, :hint, :code, :raw_message. The shape is suitable for rendering in the dashboard.
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
# File 'lib/pg_reports/connection/error_translator.rb', line 26 def translate(error) sqlstate = sqlstate_for(error) = error..to_s info = case sqlstate when "42501" then () when "3D000" then database_does_not_exist() when "28000", "28P01" then auth_failed() when "08001", "08006", "08000", "08003", "08004" then connection_refused() when "53300" then too_many_connections() when "42P01" then undefined_table() when "42883" then undefined_function() else generic(error) end info.merge(code: sqlstate, raw_message: ) end |
.undefined_function(message) ⇒ Object
129 130 131 132 133 134 135 136 |
# File 'lib/pg_reports/connection/error_translator.rb', line 129 def undefined_function() target = extract_object(, /function ([\w.]+)\(/) { title: "Function not found", detail: target ? "The selected database has no function named \"#{target}\"." : "The report called a function that does not exist on the selected database.", hint: "It is usually provided by an extension that is not installed on this database." } end |
.undefined_table(message) ⇒ Object
A report asked for a relation this database doesn't have. Almost always an extension that exists on one database in the cluster but not the one currently selected — extensions are per-database, the dashboard is not.
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/pg_reports/connection/error_translator.rb', line 110 def undefined_table() target = extract_object(, /relation "([^"]+)" does not exist/) if target && EXTENSION_RELATIONS.key?(target) extension = EXTENSION_RELATIONS.fetch(target) return { title: "#{extension} not available", detail: "This report reads \"#{target}\", which does not exist on the selected database.", hint: "CREATE EXTENSION IF NOT EXISTS #{extension};" } end { title: "Relation not found", detail: target ? "The selected database has no relation named \"#{target}\"." : "The report referenced a relation that does not exist on the selected database.", hint: nil } end |