262 lines
6.9 KiB
Go
262 lines
6.9 KiB
Go
|
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
|
||
|
}
|