go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/errutil/stack_trace.go (about)

     1  /*
     2  
     3  Copyright (c) 2023 - Present. Will Charczuk. All rights reserved.
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository.
     5  
     6  */
     7  
     8  package errutil
     9  
    10  import (
    11  	"encoding/json"
    12  	"fmt"
    13  	"path"
    14  	"runtime"
    15  	"strings"
    16  )
    17  
    18  // Defaults for start depth.
    19  const (
    20  	DefaultStackTraceStartDepth = 3
    21  	DefaultErrorStartDepth      = 4
    22  )
    23  
    24  // StackTraceProvider is a type that can return an exception class.
    25  type StackTraceProvider interface {
    26  	StackTrace() StackTrace
    27  }
    28  
    29  // GetStackTrace is a utility method to get the current stack trace at call time.
    30  func GetStackTrace() string {
    31  	return fmt.Sprintf("%+v", Callers(DefaultStackTraceStartDepth))
    32  }
    33  
    34  // Callers returns stack pointers.
    35  func Callers(startDepth int) StackPointers {
    36  	const depth = 32
    37  	var pcs [depth]uintptr
    38  	n := runtime.Callers(startDepth, pcs[:])
    39  	var st StackPointers = pcs[0:n]
    40  	return st
    41  }
    42  
    43  // StackTrace is a stack trace provider.
    44  type StackTrace interface {
    45  	fmt.Formatter
    46  	Strings() []string
    47  	String() string
    48  }
    49  
    50  // StackPointers is stack of uintptr stack frames from innermost (newest) to outermost (oldest).
    51  type StackPointers []uintptr
    52  
    53  // Format formats the stack trace.
    54  func (st StackPointers) Format(s fmt.State, verb rune) {
    55  	switch verb {
    56  	case 'v':
    57  		switch {
    58  		case s.Flag('+'):
    59  			for _, f := range st {
    60  				fmt.Fprintf(s, "\n%+v", Frame(f))
    61  			}
    62  		case s.Flag('#'):
    63  			for _, f := range st {
    64  				fmt.Fprintf(s, "\n%#v", Frame(f))
    65  			}
    66  		default:
    67  			for _, f := range st {
    68  				fmt.Fprintf(s, "\n%v", Frame(f))
    69  			}
    70  		}
    71  	case 's':
    72  		for _, f := range st {
    73  			fmt.Fprintf(s, "\n%s", Frame(f))
    74  		}
    75  	}
    76  }
    77  
    78  // Strings dereferences the StackTrace as a string slice
    79  func (st StackPointers) Strings() []string {
    80  	res := make([]string, len(st))
    81  	for i, frame := range st {
    82  		res[i] = fmt.Sprintf("%+v", Frame(frame))
    83  	}
    84  	return res
    85  }
    86  
    87  // String returns a single string representation of the stack pointers.
    88  func (st StackPointers) String() string {
    89  	return fmt.Sprintf("%+v", st)
    90  }
    91  
    92  // MarshalJSON is a custom json marshaler.
    93  func (st StackPointers) MarshalJSON() ([]byte, error) {
    94  	return json.Marshal(st.Strings())
    95  }
    96  
    97  // StackStrings represents a stack trace as string literals.
    98  type StackStrings []string
    99  
   100  // Format formats the stack trace.
   101  func (ss StackStrings) Format(s fmt.State, verb rune) {
   102  	switch verb {
   103  	case 'v':
   104  		switch {
   105  		case s.Flag('+'):
   106  			for _, f := range ss {
   107  				fmt.Fprintf(s, "\n%+v", f)
   108  			}
   109  		case s.Flag('#'):
   110  			fmt.Fprintf(s, "%#v", []string(ss))
   111  		default:
   112  			for _, f := range ss {
   113  				fmt.Fprintf(s, "\n%v", f)
   114  			}
   115  		}
   116  	case 's':
   117  		for _, f := range ss {
   118  			fmt.Fprintf(s, "\n%v", f)
   119  		}
   120  	}
   121  }
   122  
   123  // Strings returns the stack strings as a string slice.
   124  func (ss StackStrings) Strings() []string {
   125  	return []string(ss)
   126  }
   127  
   128  // String returns a single string representation of the stack pointers.
   129  func (ss StackStrings) String() string {
   130  	return fmt.Sprintf("%+v", ss)
   131  }
   132  
   133  // MarshalJSON is a custom json marshaler.
   134  func (ss StackStrings) MarshalJSON() ([]byte, error) {
   135  	return json.Marshal(ss)
   136  }
   137  
   138  // Frame represents a program counter inside a stack frame.
   139  type Frame uintptr
   140  
   141  // PC returns the program counter for this frame;
   142  // multiple frames may have the same PC value.
   143  func (f Frame) PC() uintptr { return uintptr(f) - 1 }
   144  
   145  // File returns the full path to the file that contains the
   146  // function for this Frame's pc.
   147  func (f Frame) File() string {
   148  	fn := runtime.FuncForPC(f.PC())
   149  	if fn == nil {
   150  		return "unknown"
   151  	}
   152  	file, _ := fn.FileLine(f.PC())
   153  	return file
   154  }
   155  
   156  // Line returns the line number of source code of the
   157  // function for this Frame's pc.
   158  func (f Frame) Line() int {
   159  	fn := runtime.FuncForPC(f.PC())
   160  	if fn == nil {
   161  		return 0
   162  	}
   163  	_, line := fn.FileLine(f.PC())
   164  	return line
   165  }
   166  
   167  // Func returns the func name.
   168  func (f Frame) Func() string {
   169  	name := runtime.FuncForPC(f.PC()).Name()
   170  	return funcname(name)
   171  }
   172  
   173  // Format formats the frame according to the fmt.Formatter interface.
   174  //
   175  //	%s    source file
   176  //	%d    source line
   177  //	%n    function name
   178  //	%v    equivalent to %s:%d
   179  //
   180  // Format accepts flags that alter the printing of some verbs, as follows:
   181  //
   182  //	%+s   path of source file relative to the compile time GOPATH
   183  //	%+v   equivalent to %+s:%d
   184  func (f Frame) Format(s fmt.State, verb rune) {
   185  	switch verb {
   186  	case 's':
   187  		switch {
   188  		case s.Flag('+'):
   189  			pc := f.PC()
   190  			fn := runtime.FuncForPC(pc)
   191  			if fn == nil {
   192  				fmt.Fprint(s, "unknown")
   193  			} else {
   194  				file, _ := fn.FileLine(pc)
   195  				fname := fn.Name()
   196  				fmt.Fprintf(s, "%s\n\t%s", fname, trimGOPATH(fname, file))
   197  			}
   198  		default:
   199  			fmt.Fprint(s, path.Base(f.File()))
   200  		}
   201  	case 'd':
   202  		fmt.Fprintf(s, "%d", f.Line())
   203  	case 'n':
   204  		name := runtime.FuncForPC(f.PC()).Name()
   205  		fmt.Fprint(s, funcname(name))
   206  	case 'v':
   207  		f.Format(s, 's')
   208  		fmt.Fprint(s, ":")
   209  		f.Format(s, 'd')
   210  	}
   211  }
   212  
   213  // funcname removes the path prefix component of a function's name reported by func.Name().
   214  func funcname(name string) string {
   215  	i := strings.LastIndex(name, "/")
   216  	name = name[i+1:]
   217  	i = strings.Index(name, ".")
   218  	return name[i+1:]
   219  }
   220  
   221  func trimGOPATH(name, file string) string {
   222  	// Here we want to get the source file path relative to the compile time
   223  	// GOPATH. As of Go 1.6.x there is no direct way to know the compiled
   224  	// GOPATH at runtime, but we can infer the number of path segments in the
   225  	// GOPATH. We note that fn.Name() returns the function name qualified by
   226  	// the import path, which does not include the GOPATH. Thus we can trim
   227  	// segments from the beginning of the file path until the number of path
   228  	// separators remaining is one more than the number of path separators in
   229  	// the function name. For example, given:
   230  	//
   231  	//    GOPATH     /home/user
   232  	//    file       /home/user/src/pkg/sub/file.go
   233  	//    fn.Name()  pkg/sub.Type.Method
   234  	//
   235  	// We want to produce:
   236  	//
   237  	//    pkg/sub/file.go
   238  	//
   239  	// From this we can easily see that fn.Name() has one less path separator
   240  	// than our desired output. We count separators from the end of the file
   241  	// path until it finds two more than in the function name and then move
   242  	// one character forward to preserve the initial path segment without a
   243  	// leading separator.
   244  	const sep = "/"
   245  	goal := strings.Count(name, sep) + 2
   246  	i := len(file)
   247  	for n := 0; n < goal; n++ {
   248  		i = strings.LastIndex(file[:i], sep)
   249  		if i == -1 {
   250  			// not enough separators found, set i so that the slice expression
   251  			// below leaves file unmodified
   252  			i = -len(sep)
   253  			break
   254  		}
   255  	}
   256  	// get back to 0 or trim the leading separator
   257  	file = file[i+len(sep):]
   258  	return file
   259  }