vitess.io/vitess@v0.16.2/go/tb/error.go (about)

     1  /*
     2  Copyright 2019 The Vitess Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package tb exposes some handy traceback functionality buried in the runtime.
    18  //
    19  // It can also be used to provide context to errors reducing the temptation to
    20  // panic carelessly, just to get stack information.
    21  //
    22  // The theory is that most errors that are created with the fmt.Errorf
    23  // style are likely to be rare, but require more context to debug
    24  // properly. The additional cost of computing a stack trace is
    25  // therefore negligible.
    26  package tb
    27  
    28  import (
    29  	"bytes"
    30  	"fmt"
    31  	"os"
    32  	"runtime"
    33  )
    34  
    35  var (
    36  	dunno     = []byte("???")
    37  	centerDot = []byte("·")
    38  	dot       = []byte(".")
    39  )
    40  
    41  // StackError represents an error along with a stack trace.
    42  type StackError interface {
    43  	Error() string
    44  	StackTrace() string
    45  }
    46  
    47  type stackError struct {
    48  	err        error
    49  	stackTrace string
    50  }
    51  
    52  func (e stackError) Error() string {
    53  	return fmt.Sprintf("%v\n%v", e.err, e.stackTrace)
    54  }
    55  
    56  func (e stackError) StackTrace() string {
    57  	return e.stackTrace
    58  }
    59  
    60  func Errorf(msg string, args ...any) error {
    61  	stack := ""
    62  	// See if any arg is already embedding a stack - no need to
    63  	// recompute something expensive and make the message unreadable.
    64  	for _, arg := range args {
    65  		if stackErr, ok := arg.(stackError); ok {
    66  			stack = stackErr.stackTrace
    67  			break
    68  		}
    69  	}
    70  
    71  	if stack == "" {
    72  		// magic 5 trims off just enough stack data to be clear
    73  		stack = string(Stack(5))
    74  	}
    75  
    76  	return stackError{fmt.Errorf(msg, args...), stack}
    77  }
    78  
    79  // Stack is taken from runtime/debug.go
    80  // calldepth is the number of (bottommost) frames to skip.
    81  func Stack(calldepth int) []byte {
    82  	return stack(calldepth)
    83  }
    84  
    85  func stack(calldepth int) []byte {
    86  	buf := new(bytes.Buffer) // the returned data
    87  	// As we loop, we open files and read them. These variables record the currently
    88  	// loaded file.
    89  	var lines [][]byte
    90  	var lastFile string
    91  	for i := calldepth; ; i++ { // Caller we care about is the user, 2 frames up
    92  		pc, file, line, ok := runtime.Caller(i)
    93  		if !ok {
    94  			break
    95  		}
    96  		// Print this much at least.  If we can't find the source, it won't show.
    97  		fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
    98  		if file != lastFile {
    99  			data, err := os.ReadFile(file)
   100  			if err != nil {
   101  				continue
   102  			}
   103  			lines = bytes.Split(data, []byte{'\n'})
   104  			lastFile = file
   105  		}
   106  		line-- // in stack trace, lines are 1-indexed but our array is 0-indexed
   107  		fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line))
   108  	}
   109  	return buf.Bytes()
   110  }
   111  
   112  // source returns a space-trimmed slice of the n'th line.
   113  func source(lines [][]byte, n int) []byte {
   114  	if n < 0 || n >= len(lines) {
   115  		return dunno
   116  	}
   117  	return bytes.Trim(lines[n], " \t")
   118  }
   119  
   120  // function returns, if possible, the name of the function containing the PC.
   121  func function(pc uintptr) []byte {
   122  	fn := runtime.FuncForPC(pc)
   123  	if fn == nil {
   124  		return dunno
   125  	}
   126  	name := []byte(fn.Name())
   127  	// The name includes the path name to the package, which is unnecessary
   128  	// since the file name is already included.  Plus, it has center dots.
   129  	// That is, we see
   130  	//	runtime/debug.*T·ptrmethod
   131  	// and want
   132  	//	*T.ptrmethod
   133  	if period := bytes.Index(name, dot); period >= 0 {
   134  		name = name[period+1:]
   135  	}
   136  	name = bytes.Replace(name, centerDot, dot, -1)
   137  	return name
   138  }