github.com/blend/go-sdk@v1.20220411.3/ex/stack_trace.go (about)

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