-
Notifications
You must be signed in to change notification settings - Fork 0
/
tracer.go
205 lines (173 loc) · 6.02 KB
/
tracer.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
package xtrace
/*
Copyright 2019 Nicholas Krichevsky
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import (
"bytes"
"fmt"
"io"
"sync"
"golang.org/x/xerrors"
)
const emptyError = "<empty>"
// Tracer gets the trace of errors wrapped by xerrors.
type Tracer struct {
detailedOutput bool
// Populated with the full chain of errors, with the originating error at len(errorChain) - 1
errorChain []error
// Holds the contents of the current error being read
buffer *bytes.Buffer
// Formats the traces returned by the Read functions
formatter TraceFormatter
// Sets the order of the method
ordering TraceOrderingMethod
// baseError is the original error passed, primarily used for cloning purposes
baseErr error
// holds all of the option functions passed to the tracer, primarily used for cloning purposes
optionFuncs []func(*Tracer) error
// ensures that only one read can take place at a time
readMux sync.Mutex
}
// NewTracer returns a new Tracer for the given error.
func NewTracer(baseErr error, options ...func(*Tracer) error) (*Tracer, error) {
formatter, err := NewNewLineFormatter(Naive(false))
if err != nil {
return nil, xerrors.Errorf("Could not construct formatter for Tracer: %w")
}
tracer := &Tracer{
errorChain: buildErrorChain(baseErr),
detailedOutput: true,
buffer: bytes.NewBuffer([]byte{}),
formatter: formatter,
ordering: OldestFirstOrdering,
baseErr: baseErr,
optionFuncs: options,
}
for _, optionFunc := range options {
err := optionFunc(tracer)
if err != nil {
return nil, xerrors.Errorf("Could not construct Tracer: %w", err)
}
}
return tracer, nil
}
// buildErrChain builds a slice of all of the errors with the oldest at the back of the list.
func buildErrorChain(baseErr error) []error {
chain := []error{}
errCursor := baseErr
for errCursor != nil {
chain = append(chain, errCursor)
errCursor = xerrors.Unwrap(errCursor)
}
return chain
}
// Read implements the io.Reader interface. Will read up to len(dest) bytes of the current error.
// Note that this means dest will only be filled up the contents of the error, regardless of if there are other errors
// to be read in the error stack.
// Returns io.EOF when there are no more errors to read, but notably will not be returned when the last error is
// returned.
func (tracer *Tracer) Read(dest []byte) (n int, err error) {
tracer.readMux.Lock()
defer tracer.readMux.Unlock()
if tracer.buffer.Len() == 0 && len(tracer.errorChain) == 0 {
return 0, io.EOF
} else if tracer.buffer.Len() == 0 {
message := generateErrorString(tracer.popChain(), tracer.formatter, tracer.detailedOutput)
// If we are passed a zero length error, returning an io.EOF is not appropriate.
if len(message) == 0 {
message = emptyError
}
tracer.buffer.WriteString(message)
}
return tracer.buffer.Read(dest)
}
// ReadNext will read one unwrapped error and its associated trace
// If Read() has been called, but the buffer has not been exhausted, its contents will be discarded.
// Returns io.EOF when there are no more errors to read, but notably will not be returned when the last error is
// returned.
func (tracer *Tracer) ReadNext() (string, error) {
tracer.readMux.Lock()
defer tracer.readMux.Unlock()
tracer.buffer.Reset()
if len(tracer.errorChain) == 0 {
return "", io.EOF
}
message := generateErrorString(tracer.popChain(), tracer.formatter, tracer.detailedOutput)
if len(message) == 0 {
return emptyError, nil
}
return message, nil
}
// popChain will pop the next error off the error chain
func (tracer *Tracer) popChain() (storedError error) {
if tracer.ordering == OldestFirstOrdering {
storedError = tracer.errorChain[len(tracer.errorChain)-1]
tracer.errorChain = tracer.errorChain[:len(tracer.errorChain)-1]
} else {
storedError = tracer.errorChain[0]
tracer.errorChain = tracer.errorChain[1:]
}
return
}
// Format allows for tracer to implement fmt.Formatter. This will simply make a clone of the tracer
// and print out the full trace. DetailedOutput will be given when %+v is provided, and normal output
// when %v is provided.
func (tracer *Tracer) Format(s fmt.State, verb rune) {
if verb != 'v' {
return
}
clone, err := NewTracer(tracer.baseErr, tracer.optionFuncs...)
if err != nil {
out := fmt.Sprintf("<could not print trace: %s>", err)
io.WriteString(s, out)
return
}
clone.detailedOutput = s.Flag('+')
err = clone.trace(s)
if err != nil {
out := fmt.Sprintf("<%s>", err)
io.WriteString(s, out)
return
}
}
// Trace makes a clone of the Tracer and writes the full trace to the provided io.Writer.
func (tracer *Tracer) Trace(writer io.Writer) error {
clone, err := NewTracer(tracer.baseErr, tracer.optionFuncs...)
if err != nil {
return xerrors.Errorf("failed to recreate Tracer for re-tracing: %w", err)
}
return clone.trace(writer)
}
// trace is identical to Trace, but does not clone the Tracer.
func (tracer *Tracer) trace(writer io.Writer) error {
err := tracer.writeRemainingErrors(writer)
if err != nil {
return xerrors.Errorf("failed to write trace to writer: %w", err)
}
return nil
}
// writeRemainingErrors will write all errors left in the tracer to the given io.Writer
func (tracer *Tracer) writeRemainingErrors(writer io.Writer) error {
lastOutput := ""
for {
out, err := tracer.ReadNext()
if err != nil && err != io.EOF {
return xerrors.Errorf("could not read trace: %w", err)
} else if err == io.EOF {
io.WriteString(writer, lastOutput[:len(lastOutput)-1])
return nil
} else {
io.WriteString(writer, lastOutput)
lastOutput = out + "\n"
}
}
}