package log import ( "context" "fmt" "log/slog" "strings" "github.com/pkg/errors" ) const tab = "\t" const fourSpaces = " " // TextHandler is a slog.Handler that replaces all \t instances with four spaces and replaces errors with their // detailed string representation. type TextHandler struct { slog.Handler } // Handle replaces all \t instances with four spaces and replaces errors with their detailed string representation. func (handler *TextHandler) Handle(ctx context.Context, record slog.Record) error { record.Message = strings.ReplaceAll(record.Message, tab, fourSpaces) record = handler.parseRecordAttrs(record) return handler.Handler.Handle(ctx, record) } func (handler *TextHandler) parseRecordAttrs(record slog.Record) slog.Record { var attrs []slog.Attr record.Attrs(func(attr slog.Attr) bool { newAttrs := handler.parseAttr(attr) for _, newAttr := range newAttrs { attrs = append(attrs, *newAttr) } return true }) newRecord := slog.NewRecord(record.Time, record.Level, record.Message, record.PC) newRecord.AddAttrs(attrs...) return newRecord } func (handler *TextHandler) parseAttr(attr slog.Attr) []*slog.Attr { newAttrs := []*slog.Attr{&attr} attr.Value = attr.Value.Resolve() switch attr.Value.Kind() { case slog.KindString: attr.Value = slog.StringValue(strings.ReplaceAll(attr.Value.String(), tab, fourSpaces)) case slog.KindAny: if err, ok := attr.Value.Any().(error); ok { newAttrs = handler.parseErrorAttr(attr, err) } case slog.KindGroup: var groupAttrs []slog.Attr for _, groupAttr := range attr.Value.Group() { newGroupAttrs := handler.parseAttr(groupAttr) for _, newGroupAttr := range newGroupAttrs { groupAttrs = append(groupAttrs, *newGroupAttr) } } attr.Value = slog.GroupValue(groupAttrs...) } return newAttrs } func (handler *TextHandler) parseErrorAttr(attr slog.Attr, err error) []*slog.Attr { attr.Value = slog.StringValue(fmt.Sprintf("%+v", err)) newAttrs := handler.parseAttr(attr) httpError, ok := errors.Cause(err).(HTTPError) if ok { if responseBody, err := httpError.ReadBody(); err == nil { responseBodyAttr := slog.String("response body", responseBody) newAttrs = append(newAttrs, &responseBodyAttr) } } return newAttrs } // WithAttrs returns a new TextHandler with the given attributes appended to the handler's existing attributes. func (handler *TextHandler) WithAttrs(attrs []slog.Attr) slog.Handler { return &TextHandler{handler.Handler.WithAttrs(attrs)} } // WithGroup returns a new TextHandler with the given group name set. func (handler *TextHandler) WithGroup(name string) slog.Handler { return &TextHandler{handler.Handler.WithGroup(name)} }