File: callstack.go

package info (click to toggle)
golang-github-mgutz-logxi 1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 524 kB
  • sloc: makefile: 7
file content (261 lines) | stat: -rw-r--r-- 7,087 bytes parent folder | download
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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
package log

import (
	"bufio"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/mgutz/ansi"
)

type sourceLine struct {
	lineno int
	line   string
}

type frameInfo struct {
	filename     string
	lineno       int
	method       string
	context      []*sourceLine
	contextLines int
}

func (ci *frameInfo) readSource(contextLines int) error {
	if ci.lineno == 0 || disableCallstack {
		return nil
	}
	start := maxInt(1, ci.lineno-contextLines)
	end := ci.lineno + contextLines

	f, err := os.Open(ci.filename)
	if err != nil {
		// if we can't read a file, it means user is running this in production
		disableCallstack = true
		return err
	}
	defer f.Close()

	lineno := 1
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		if start <= lineno && lineno <= end {
			line := scanner.Text()
			line = expandTabs(line, 4)
			ci.context = append(ci.context, &sourceLine{lineno: lineno, line: line})
		}
		lineno++
	}

	if err := scanner.Err(); err != nil {
		InternalLog.Warn("scanner error", "file", ci.filename, "err", err)
	}
	return nil
}

func (ci *frameInfo) String(color string, sourceColor string) string {
	buf := pool.Get()
	defer pool.Put(buf)

	if disableCallstack {
		buf.WriteString(color)
		buf.WriteString(Separator)
		buf.WriteString(indent)
		buf.WriteString(ci.filename)
		buf.WriteRune(':')
		buf.WriteString(strconv.Itoa(ci.lineno))
		return buf.String()
	}

	// skip anything in the logxi package
	if isLogxiCode(ci.filename) {
		return ""
	}

	// make path relative to current working directory or home
	tildeFilename, err := filepath.Rel(wd, ci.filename)
	if err != nil {
		InternalLog.Warn("Could not make path relative", "path", ci.filename)
		return ""
	}
	// ../../../ is too complex.  Make path relative to home
	if strings.HasPrefix(tildeFilename, strings.Repeat(".."+string(os.PathSeparator), 3)) {
		tildeFilename = strings.Replace(tildeFilename, home, "~", 1)
	}

	buf.WriteString(color)
	buf.WriteString(Separator)
	buf.WriteString(indent)
	buf.WriteString("in ")
	buf.WriteString(ci.method)
	buf.WriteString("(")
	buf.WriteString(tildeFilename)
	buf.WriteRune(':')
	buf.WriteString(strconv.Itoa(ci.lineno))
	buf.WriteString(")")

	if ci.contextLines == -1 {
		return buf.String()
	}
	buf.WriteString("\n")

	// the width of the printed line number
	var linenoWidth int
	// trim spaces at start of source code based on common spaces
	var skipSpaces = 1000

	// calculate width of lineno and number of leading spaces that can be
	// removed
	for _, li := range ci.context {
		linenoWidth = maxInt(linenoWidth, len(fmt.Sprintf("%d", li.lineno)))
		index := indexOfNonSpace(li.line)
		if index > -1 && index < skipSpaces {
			skipSpaces = index
		}
	}

	for _, li := range ci.context {
		var format string
		format = fmt.Sprintf("%%s%%%dd:  %%s\n", linenoWidth)

		if li.lineno == ci.lineno {
			buf.WriteString(color)
			if ci.contextLines > 2 {
				format = fmt.Sprintf("%%s=> %%%dd:  %%s\n", linenoWidth)
			}
		} else {
			buf.WriteString(sourceColor)
			if ci.contextLines > 2 {
				// account for "=> "
				format = fmt.Sprintf("%%s%%%dd:  %%s\n", linenoWidth+3)
			}
		}
		// trim spaces at start
		idx := minInt(len(li.line), skipSpaces)
		buf.WriteString(fmt.Sprintf(format, Separator+indent+indent, li.lineno, li.line[idx:]))
	}
	// get rid of last \n
	buf.Truncate(buf.Len() - 1)
	if !disableColors {
		buf.WriteString(ansi.Reset)
	}
	return buf.String()
}

// parseDebugStack parases a stack created by debug.Stack()
//
// This is what the string looks like
// /Users/mgutz/go/src/github.com/mgutz/logxi/v1/jsonFormatter.go:45 (0x5fa70)
// 	(*JSONFormatter).writeError: jf.writeString(buf, err.Error()+"\n"+string(debug.Stack()))
// /Users/mgutz/go/src/github.com/mgutz/logxi/v1/jsonFormatter.go:82 (0x5fdc3)
// 	(*JSONFormatter).appendValue: jf.writeError(buf, err)
// /Users/mgutz/go/src/github.com/mgutz/logxi/v1/jsonFormatter.go:109 (0x605ca)
// 	(*JSONFormatter).set: jf.appendValue(buf, val)
// ...
// /Users/mgutz/goroot/src/runtime/asm_amd64.s:2232 (0x38bf1)
// 	goexit:
func parseDebugStack(stack string, skip int, ignoreRuntime bool) []*frameInfo {
	frames := []*frameInfo{}
	// BUG temporarily disable since there is a bug with embedded newlines
	if true {
		return frames
	}

	lines := strings.Split(stack, "\n")

	for i := skip * 2; i < len(lines); i += 2 {
		ci := &frameInfo{}
		sourceLine := lines[i]
		if sourceLine == "" {
			break
		}
		if ignoreRuntime && strings.Contains(sourceLine, filepath.Join("src", "runtime")) {
			break
		}

		colon := strings.Index(sourceLine, ":")
		slash := strings.Index(sourceLine, "/")
		if colon < slash {
			// must be on Windows where paths look like c:/foo/bar.go:lineno
			colon = strings.Index(sourceLine[slash:], ":") + slash
		}
		space := strings.Index(sourceLine, " ")
		ci.filename = sourceLine[0:colon]

		// BUG with callstack where the error message has embedded newlines
		// if colon > space {
		// 	fmt.Println("lines", lines)
		// }
		// fmt.Println("SOURCELINE", sourceLine, "len", len(sourceLine), "COLON", colon, "SPACE", space)
		numstr := sourceLine[colon+1 : space]
		lineno, err := strconv.Atoi(numstr)
		if err != nil {
			InternalLog.Warn("Could not parse line number", "sourceLine", sourceLine, "numstr", numstr)
			continue
		}
		ci.lineno = lineno

		methodLine := lines[i+1]
		colon = strings.Index(methodLine, ":")
		ci.method = strings.Trim(methodLine[0:colon], "\t ")
		frames = append(frames, ci)
	}
	return frames
}

// parseDebugStack parases a stack created by debug.Stack()
//
// This is what the string looks like
// /Users/mgutz/go/src/github.com/mgutz/logxi/v1/jsonFormatter.go:45 (0x5fa70)
// 	(*JSONFormatter).writeError: jf.writeString(buf, err.Error()+"\n"+string(debug.Stack()))
// /Users/mgutz/go/src/github.com/mgutz/logxi/v1/jsonFormatter.go:82 (0x5fdc3)
// 	(*JSONFormatter).appendValue: jf.writeError(buf, err)
// /Users/mgutz/go/src/github.com/mgutz/logxi/v1/jsonFormatter.go:109 (0x605ca)
// 	(*JSONFormatter).set: jf.appendValue(buf, val)
// ...
// /Users/mgutz/goroot/src/runtime/asm_amd64.s:2232 (0x38bf1)
// 	goexit:
func trimDebugStack(stack string) string {
	buf := pool.Get()
	defer pool.Put(buf)
	lines := strings.Split(stack, "\n")
	for i := 0; i < len(lines); i += 2 {
		sourceLine := lines[i]
		if sourceLine == "" {
			break
		}

		colon := strings.Index(sourceLine, ":")
		slash := strings.Index(sourceLine, "/")
		if colon < slash {
			// must be on Windows where paths look like c:/foo/bar.go:lineno
			colon = strings.Index(sourceLine[slash:], ":") + slash
		}
		filename := sourceLine[0:colon]
		// skip anything in the logxi package
		if isLogxiCode(filename) {
			continue
		}
		buf.WriteString(sourceLine)
		buf.WriteRune('\n')
		buf.WriteString(lines[i+1])
		buf.WriteRune('\n')
	}
	return buf.String()
}

func parseLogxiStack(entry map[string]interface{}, skip int, ignoreRuntime bool) []*frameInfo {
	kv := entry[KeyMap.CallStack]
	if kv == nil {
		return nil
	}

	var frames []*frameInfo
	if stack, ok := kv.(string); ok {
		frames = parseDebugStack(stack, skip, ignoreRuntime)
	}
	return frames
}