error.go 1.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
  1. package util
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "log/slog"
  7. )
  8. type ContextualError struct {
  9. RealError error
  10. Fields map[string]any
  11. Context string
  12. }
  13. func NewContextualError(msg string, fields map[string]any, realError error) *ContextualError {
  14. return &ContextualError{Context: msg, Fields: fields, RealError: realError}
  15. }
  16. // ContextualizeIfNeeded is a helper function to turn an error into a ContextualError if it is not already one
  17. func ContextualizeIfNeeded(msg string, err error) error {
  18. switch err.(type) {
  19. case *ContextualError:
  20. return err
  21. default:
  22. return NewContextualError(msg, nil, err)
  23. }
  24. }
  25. // LogWithContextIfNeeded is a helper function to log an error line for an error or ContextualError
  26. func LogWithContextIfNeeded(msg string, err error, l *slog.Logger) {
  27. switch v := err.(type) {
  28. case *ContextualError:
  29. v.Log(l)
  30. default:
  31. l.Error(msg, "error", err)
  32. }
  33. }
  34. func (ce *ContextualError) Error() string {
  35. if ce.RealError == nil {
  36. return ce.Context
  37. }
  38. return fmt.Errorf("%s (%v): %w", ce.Context, ce.Fields, ce.RealError).Error()
  39. }
  40. func (ce *ContextualError) Unwrap() error {
  41. if ce.RealError == nil {
  42. return errors.New(ce.Context)
  43. }
  44. return ce.RealError
  45. }
  46. // Log emits ce as a single error-level log line with Fields and RealError
  47. // promoted to top-level attributes, producing a flat shape callers can grep
  48. // or parse without walking into a nested object.
  49. func (ce *ContextualError) Log(l *slog.Logger) {
  50. attrs := make([]slog.Attr, 0, len(ce.Fields)+1)
  51. for k, v := range ce.Fields {
  52. attrs = append(attrs, slog.Any(k, v))
  53. }
  54. if ce.RealError != nil {
  55. attrs = append(attrs, slog.Any("error", ce.RealError))
  56. }
  57. // LogAttrs is intentional: attrs is built from a map[string]any so it has
  58. // no pair-form equivalent.
  59. //nolint:sloglint
  60. l.LogAttrs(context.Background(), slog.LevelError, ce.Context, attrs...)
  61. }