diff --git a/slogex.go b/slogex.go new file mode 100644 index 0000000..fa7bb89 --- /dev/null +++ b/slogex.go @@ -0,0 +1,45 @@ +package stacktrace + +import ( + "fmt" + "log/slog" +) + +func ErrToSlogAttr(err error, opts ...TracesOpt) slog.Attr { + if err == nil { + return slog.Attr{} + } + + st, ok := Unwrap(err) + if !ok { + return slog.String("error", err.Error()) + } + + if st == nil { + return slog.String("error", "nil stacktrace") + } + + tracebacks := st.GetTraces(opts...) + + tracebackAttrs := make([]slog.Attr, 0, len(tracebacks)) + for traceIndex := range tracebacks { + traceback := tracebacks[traceIndex] + stackAttrs := make([]slog.Attr, 0, len(traceback.Stack)) + for stackIndex := range traceback.Stack { + stack := traceback.Stack[stackIndex] + key := fmt.Sprintf("%d", stackIndex) + stackAttr := slog.Group( + key, + slog.String("type", string(stack.Type)), + slog.String("severity", string(stack.Severity)), + slog.String("position", stack.LinePos), + slog.String("message", stack.Message), + ) + stackAttrs = append(stackAttrs, stackAttr) + } + tracebackAttr := slog.Group(fmt.Sprintf("%d", traceIndex), "stack", stackAttrs) + tracebackAttrs = append(tracebackAttrs, tracebackAttr) + } + + return slog.Group("tracebacks", "traces", tracebackAttrs) +} diff --git a/sprint.go b/sprint.go deleted file mode 100644 index 862013a..0000000 --- a/sprint.go +++ /dev/null @@ -1,123 +0,0 @@ -package stacktrace - -import "fmt" - -const ( - DefaultMessageDelimiter = " * " - DefaultTraceDelimiter = "\n\n" - DefaultStackDelimiter = "\n |_ " - DefaultEnsureDuplicates = false -) - -type SprintOpt interface { - Apply(o *SprintOptions) -} - -type SprintOptions struct { - // MessageDelimiter is a delimiter between message and stack trace - MessageDelimiter string - // TraceDelimiter is a delimiter between stack traces - TraceDelimiter string - // StackDelimiter is a delimiter between stack trace elements - StackDelimiter string - // EnsureDuplicates ensures that duplicates are not printed - EnsureDuplicates bool - dups map[string]struct{} -} - -func NewSprintOptions() *SprintOptions { - opts := &SprintOptions{ - EnsureDuplicates: DefaultEnsureDuplicates, - dups: make(map[string]struct{}), - TraceDelimiter: DefaultTraceDelimiter, - MessageDelimiter: DefaultMessageDelimiter, - StackDelimiter: DefaultStackDelimiter, - } - return opts -} - -type messageDelimiterOpt string - -func (v messageDelimiterOpt) Apply(o *SprintOptions) { - o.MessageDelimiter = string(v) -} - -type traceDelimiterOpt string - -func (v traceDelimiterOpt) Apply(o *SprintOptions) { - o.TraceDelimiter = string(v) -} - -type stackDelimiterOpt string - -func (v stackDelimiterOpt) Apply(o *SprintOptions) { - o.StackDelimiter = string(v) -} - -type ensureDuplicatesOpt struct{} - -func (ensureDuplicatesOpt) Apply(o *SprintOptions) { - o.EnsureDuplicates = true -} - -func WithMessageDelimiter(delimiter string) SprintOpt { - return messageDelimiterOpt(delimiter) -} - -func WithTraceDelimiter(delimiter string) SprintOpt { - return traceDelimiterOpt(delimiter) -} - -func WithStackDelimiter(delimiter string) SprintOpt { - return stackDelimiterOpt(delimiter) -} - -func WithEnsureDuplicates() SprintOpt { - return &ensureDuplicatesOpt{} -} - -func (st *StackTrace) sprint(opts *SprintOptions) string { - trace := st.Header() - trace = fmt.Sprintf("%s%s%s", trace, opts.MessageDelimiter, st.FullMessageWithInfo()) - - listTraces := "" - for _, elem := range st.List { - elemStr := elem.sprint(opts) - if elemStr == "" { - continue - } - if listTraces != "" { - listTraces = fmt.Sprintf("%s%s%s", listTraces, opts.TraceDelimiter, elemStr) - } else { - listTraces = elemStr - } - } - - if _, ok := opts.dups[trace]; ok { - return listTraces - } - - if st.Wrapped != nil { - wrappedStr := st.Wrapped.sprint(opts) - if wrappedStr == "" { - return listTraces - } - trace = fmt.Sprintf("%s%s%s", trace, opts.StackDelimiter, wrappedStr) - } else if opts.EnsureDuplicates { - opts.dups[trace] = struct{}{} - } - - if listTraces != "" { - trace = fmt.Sprintf("%s%s%s", trace, opts.TraceDelimiter, listTraces) - } - return trace -} - -func (st *StackTrace) Sprint(opts ...SprintOpt) string { - o := NewSprintOptions() - for _, opt := range opts { - opt.Apply(o) - } - res := st.sprint(o) - return res -} diff --git a/sprint_test.go b/sprint_test.go deleted file mode 100644 index 38bbce1..0000000 --- a/sprint_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package stacktrace - -import "testing" - -func TestError_Sprintf(t *testing.T) { - type fields struct { - Severity Severity - Type Type - Location string - Position *Position - Wrapped *StackTrace - Err error - Message string - WrappingMessage string - Info StructInfo - } - tests := []struct { - name string - fields fields - want string - }{ - { - name: "Test simple", - fields: fields{ - Severity: SeverityError, - Type: TypeValidating, - Location: "/tmp/location.raml", - Message: "error message", - }, - want: "[error] validating: /tmp/location.raml:1\n\terror message", - }, - { - name: "Test with wrapped", - fields: fields{ - Severity: SeverityError, - Type: TypeValidating, - Location: "/tmp/location.raml", - Position: &Position{1, 2}, - Message: "error message", - WrappingMessage: "wrapping message", - Wrapped: &StackTrace{ - Severity: SeverityCritical, - Type: TypeParsing, - Location: "/tmp/location2.raml", - Position: &Position{3, 4}, - Message: "error message 2", - }, - }, - want: "[error] validating: /tmp/location.raml:1:2\n\twrapping message: error message\n[critical] parsing: /tmp/location2.raml:3:4\n\terror message 2", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - e := &StackTrace{ - Severity: tt.fields.Severity, - Type: tt.fields.Type, - Location: tt.fields.Location, - Position: tt.fields.Position, - Wrapped: tt.fields.Wrapped, - Err: tt.fields.Err, - Message: tt.fields.Message, - WrappingMessage: tt.fields.WrappingMessage, - Info: tt.fields.Info, - } - if got := e.Sprint(); got != tt.want { - t.Errorf("Sprint() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/stacktrace.go b/stacktrace.go index 78a0996..4642e6e 100644 --- a/stacktrace.go +++ b/stacktrace.go @@ -185,13 +185,8 @@ func (st *StackTrace) Header() string { result := fmt.Sprintf("[%s] %s: %s", st.Severity, st.Type, - st.Location, + st.GetLocWithPos(), ) - if st.Position != nil { - result = fmt.Sprintf("%s:%d:%d", result, st.Position.Line, st.Position.Column) - } else { - result = fmt.Sprintf("%s:1", result) - } return result } @@ -442,6 +437,17 @@ func (st *StackTrace) Clone() *StackTrace { return &c } +// GetLocWithPos returns the location with position of the StackTrace. +func (st *StackTrace) GetLocWithPos() string { + result := st.Location + if st.Position != nil { + result = fmt.Sprintf("%s:%d:%d", result, st.Position.Line, st.Position.Column) + } else { + result = fmt.Sprintf("%s:1", result) + } + return result +} + // Position contains the line and column where the error occurred. type Position struct { Line int diff --git a/traces.go b/traces.go new file mode 100644 index 0000000..2b11644 --- /dev/null +++ b/traces.go @@ -0,0 +1,96 @@ +package stacktrace + +type TracesOpt interface { + Apply(o *TracesOptions) +} + +type TracesOptions struct { + // EnsureDuplicates ensures that duplicates are not printed + EnsureDuplicates bool + dupLocs map[string]struct{} +} + +func NewTracesOptions() *TracesOptions { + opts := &TracesOptions{ + EnsureDuplicates: false, + dupLocs: make(map[string]struct{}), + } + return opts +} + +type ensureDuplicatesOpt struct{} + +func (ensureDuplicatesOpt) Apply(o *TracesOptions) { + o.EnsureDuplicates = true +} + +func WithEnsureDuplicates() TracesOpt { + return &ensureDuplicatesOpt{} +} + +type Stack struct { + LinePos string + Severity Severity + Message string + Type Type +} + +func NewStack() *Stack { + return &Stack{} +} + +type Trace struct { + Stack []Stack +} + +func NewTrace() *Trace { + return &Trace{Stack: make([]Stack, 0)} +} + +func (st *StackTrace) getTraces(opts *TracesOptions) []Trace { + traces := make([]Trace, 0) + + tracesWithList := func() []Trace { + for _, elem := range st.List { + elemTraces := elem.getTraces(opts) + traces = append(traces, elemTraces...) + } + return traces + } + + trace := NewTrace() + stack := NewStack() + stack.LinePos = st.GetLocWithPos() + stack.Severity = st.Severity + stack.Message = st.FullMessageWithInfo() + stack.Type = st.Type + + if _, ok := opts.dupLocs[stack.LinePos]; ok { + return tracesWithList() + } + + trace.Stack = append(trace.Stack, *stack) + if st.Wrapped != nil { + wrappedTraces := st.Wrapped.getTraces(opts) + if len(wrappedTraces) == 0 { + return tracesWithList() + } + for i := range wrappedTraces { + trace.Stack = append(trace.Stack, wrappedTraces[i].Stack...) + } + } else if opts.EnsureDuplicates { + opts.dupLocs[stack.LinePos] = struct{}{} + } + + traces = append(traces, *trace) + + return tracesWithList() +} + +func (st *StackTrace) GetTraces(opts ...TracesOpt) []Trace { + o := NewTracesOptions() + for _, opt := range opts { + opt.Apply(o) + } + return st.getTraces(o) +} diff --git a/traces_test.go b/traces_test.go new file mode 100644 index 0000000..abef8c5 --- /dev/null +++ b/traces_test.go @@ -0,0 +1,225 @@ +package stacktrace + +import ( + "reflect" + "testing" +) + +func TestStackTrace_GetTraces(t *testing.T) { + type fields struct { + Severity Severity + Type Type + Location string + Position *Position + Wrapped *StackTrace + Err error + Message string + WrappingMessage string + Info StructInfo + List []*StackTrace + typeIsSet bool + } + type args struct { + opts []TracesOpt + } + tests := []struct { + name string + fields fields + args args + want []Trace + }{ + { + name: "Test simple", + fields: fields{ + Severity: SeverityError, + Type: TypeValidating, + Location: "/tmp/location.raml", + Message: "error message", + }, + want: []Trace{ + { + Stack: []Stack{ + { + LinePos: "/tmp/location.raml:1", + Severity: SeverityError, + Message: "error message", + Type: TypeValidating, + }, + }, + }, + }, + }, + { + name: "Test with wrapped", + fields: fields{ + Severity: SeverityError, + Type: TypeValidating, + Location: "/tmp/location.raml", + Position: &Position{1, 2}, + Message: "error message", + WrappingMessage: "wrapping message", + Wrapped: &StackTrace{ + Severity: SeverityCritical, + Type: TypeParsing, + Location: "/tmp/location2.raml", + Position: &Position{3, 4}, + Message: "error message 2", + }, + }, + want: []Trace{ + { + Stack: []Stack{ + { + LinePos: "/tmp/location.raml:1:2", + Severity: SeverityError, + Message: "wrapping message: error message", + Type: TypeValidating, + }, + { + LinePos: "/tmp/location2.raml:3:4", + Severity: SeverityCritical, + Message: "error message 2", + Type: TypeParsing, + }, + }, + }, + }, + }, + { + name: "Test with wrapped and EnsureDuplicates", + fields: fields{ + Severity: SeverityError, + Type: TypeValidating, + Location: "/tmp/location.raml", + Position: &Position{1, 2}, + Message: "error message", + WrappingMessage: "wrapping message", + Wrapped: &StackTrace{ + Severity: SeverityCritical, + Type: TypeParsing, + Location: "/tmp/location2.raml", + Position: &Position{3, 4}, + Message: "error message 2", + }, + List: []*StackTrace{ + { + Severity: SeverityCritical, + Type: TypeParsing, + Location: "/tmp/location3.raml", + Position: &Position{5, 6}, + Message: "error message 3", + WrappingMessage: "wrapping message 3", + Wrapped: &StackTrace{ + Severity: SeverityCritical, + Type: TypeParsing, + Location: "/tmp/location2.raml", // duplicate location + Position: &Position{3, 4}, // duplicate position + Message: "error message 4", + }, + }, + }, + }, + args: args{ + opts: []TracesOpt{ + WithEnsureDuplicates(), + }, + }, + want: []Trace{ + { + Stack: []Stack{ + { + LinePos: "/tmp/location.raml:1:2", + Severity: SeverityError, + Message: "wrapping message: error message", + Type: TypeValidating, + }, + { + LinePos: "/tmp/location2.raml:3:4", + Severity: SeverityCritical, + Message: "error message 2", + Type: TypeParsing, + }, + }, + }, + }, + }, + { + name: "Test with list", + fields: fields{ + Severity: SeverityError, + Type: TypeValidating, + Location: "/tmp/location.raml", + Position: &Position{1, 2}, + Message: "error message", + List: []*StackTrace{ + { + Severity: SeverityCritical, + Type: TypeParsing, + Location: "/tmp/location2.raml", + Position: &Position{3, 4}, + Message: "error message 2", + }, + { + Severity: SeverityCritical, + Type: TypeParsing, + Location: "/tmp/location3.raml", + Position: &Position{5, 6}, + Message: "error message 3", + }, + }, + }, + want: []Trace{ + { + Stack: []Stack{ + { + LinePos: "/tmp/location.raml:1:2", + Severity: SeverityError, + Message: "error message", + Type: TypeValidating, + }, + }, + }, + { + Stack: []Stack{ + { + LinePos: "/tmp/location2.raml:3:4", + Severity: SeverityCritical, + Message: "error message 2", + Type: TypeParsing, + }, + }, + }, + { + Stack: []Stack{ + { + LinePos: "/tmp/location3.raml:5:6", + Severity: SeverityCritical, + Message: "error message 3", + Type: TypeParsing, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + st := &StackTrace{ + Severity: tt.fields.Severity, + Type: tt.fields.Type, + Location: tt.fields.Location, + Position: tt.fields.Position, + Wrapped: tt.fields.Wrapped, + Err: tt.fields.Err, + Message: tt.fields.Message, + WrappingMessage: tt.fields.WrappingMessage, + Info: tt.fields.Info, + List: tt.fields.List, + typeIsSet: tt.fields.typeIsSet, + } + if got := st.GetTraces(tt.args.opts...); !reflect.DeepEqual(got, tt.want) { + t.Errorf("getTraces() = %v, want %v", got, tt.want) + } + }) + } +}