classifyDbError converts database errors into a bounded set of reason labels. During the 2026-05-13 incident the previous implementation collapsed two very different signals into a single "db_connection" label: - real pgbouncer/network transport failures (rare; needs ops attention) - eRPC's own Pos
(err error)
| 485 | // Splitting them lets us alert on the first without being drowned by the |
| 486 | // second. |
| 487 | func (s *DatabaseStrategy) classifyDbError(err error) string { |
| 488 | if err == nil { |
| 489 | return "db_query_error" |
| 490 | } |
| 491 | // Connector-internal "not ready yet" — distinct from a real transport |
| 492 | // failure because the connector has its own auto-retry loop. Surfacing |
| 493 | // this on the metrics dashboard as its own label means we can spot |
| 494 | // reconnect storms without confusing them with pgbouncer issues. |
| 495 | if errors.Is(err, data.ErrConnectorNotReady) { |
| 496 | return "db_not_ready" |
| 497 | } |
| 498 | // Timeouts |
| 499 | if errors.Is(err, context.DeadlineExceeded) || strings.Contains(err.Error(), "deadline exceeded") || strings.Contains(err.Error(), "timeout") { |
| 500 | return "db_timeout" |
| 501 | } |
| 502 | // Connection-level issues (real transport faults only). Note: we keep |
| 503 | // the substring fallback for cases where pgx wraps a transport error |
| 504 | // without preserving the typed cause, but we no longer match the bare |
| 505 | // word "connection" — see data/postgresql.go isPostgresConnectionError |
| 506 | // for the typed equivalent used by the connector itself. |
| 507 | e := err.Error() |
| 508 | if strings.Contains(e, "connection refused") || |
| 509 | strings.Contains(e, "connection reset") || |
| 510 | strings.Contains(e, "broken pipe") || |
| 511 | strings.Contains(e, "no route to host") || |
| 512 | strings.Contains(e, "EOF") || |
| 513 | strings.Contains(e, "use of closed network connection") { |
| 514 | return "db_connection" |
| 515 | } |
| 516 | return "db_query_error" |
| 517 | } |
| 518 | |
| 519 | // buildFailOpenUser returns the configured emergency user when fail-open is enabled. |
| 520 | // Returns nil when fail-open is disabled or not configured. |