github.com/mongodb/grip@v0.0.0-20240213223901-f906268d82b9/message/stack.go (about)

     1  /*
     2  Stack Messages
     3  
     4  The Stack message Composer implementations capture a full stacktrace
     5  information during message construction, and attach a message to that
     6  trace. The string form of the message includes the package and file
     7  name and line number of the last call site, while the Raw form of the
     8  message includes the entire stack. Use with an appropriate sender to
     9  capture the desired output.
    10  
    11  All stack message constructors take a "skip" parameter which tells how
    12  many stack frames to skip relative to the invocation of the
    13  constructor. Skip values less than or equal to 0 become 1, and are
    14  equal the call site of the constructor, use larger numbers if you're
    15  wrapping these constructors in our own infrastructure.
    16  
    17  In general Composers are lazy, and defer work until the message is
    18  being sent; however, the stack Composers must capture the stack when
    19  they're called rather than when they're sent to produce meaningful
    20  data.
    21  */
    22  package message
    23  
    24  import (
    25  	"fmt"
    26  	"go/build"
    27  	"path/filepath"
    28  	"runtime"
    29  	"strings"
    30  
    31  	"github.com/mongodb/grip/level"
    32  )
    33  
    34  const maxLevels = 1024
    35  
    36  // types are internal, and exposed only via the composer interface.
    37  
    38  type stackMessage struct {
    39  	Composer
    40  	trace stackFrames
    41  }
    42  
    43  // StackFrame captures a single item in a stack trace, and is used
    44  // internally and in the StackTrace output.
    45  type StackFrame struct {
    46  	Function string `bson:"function" json:"function" yaml:"function"`
    47  	File     string `bson:"file" json:"file" yaml:"file"`
    48  	Line     int    `bson:"line" json:"line" yaml:"line"`
    49  }
    50  
    51  // StackTrace structs are returned by the Raw method of the stackMessage type
    52  type StackTrace struct {
    53  	Context interface{} `bson:"context,omitempty" json:"context,omitempty" yaml:"context,omitempty"`
    54  	Frames  stackFrames `bson:"frames" json:"frames" yaml:"frames"`
    55  }
    56  
    57  func (s StackTrace) String() string { return s.Frames.String() }
    58  
    59  ////////////////////////////////////////////////////////////////////////
    60  //
    61  // Constructors for stack frame messages.
    62  //
    63  ////////////////////////////////////////////////////////////////////////
    64  
    65  // WrapStack annotates a message, converted to a composer using the
    66  // normal rules if needed, with a stack trace. Use the skip argument to
    67  // skip frames if your embedding this in your own wrapper or wrappers.
    68  func WrapStack(skip int, msg interface{}) Composer {
    69  	return &stackMessage{
    70  		trace:    captureStack(skip),
    71  		Composer: ConvertToComposer(level.Priority(0), msg),
    72  	}
    73  }
    74  
    75  // NewStack builds a Composer implementation that captures the current
    76  // stack trace with a single string message. Use the skip argument to
    77  // skip frames if your embedding this in your own wrapper or wrappers.
    78  func NewStack(skip int, message string) Composer {
    79  	return &stackMessage{
    80  		trace:    captureStack(skip),
    81  		Composer: NewString(message),
    82  	}
    83  }
    84  
    85  // NewStackLines returns a composer that builds a fmt.Println style
    86  // message that also captures a stack trace. Use the skip argument to
    87  // skip frames if your embedding this in your own wrapper or wrappers.
    88  func NewStackLines(skip int, messages ...interface{}) Composer {
    89  	return &stackMessage{
    90  		trace:    captureStack(skip),
    91  		Composer: NewLine(messages...),
    92  	}
    93  }
    94  
    95  // NewStackFormatted returns a composer that builds a fmt.Printf style
    96  // message that also captures a stack trace. Use the skip argument to
    97  // skip frames if your embedding this in your own wrapper or wrappers.
    98  func NewStackFormatted(skip int, message string, args ...interface{}) Composer {
    99  	return &stackMessage{
   100  		trace:    captureStack(skip),
   101  		Composer: NewFormatted(message, args...),
   102  	}
   103  }
   104  
   105  ////////////////////////////////////////////////////////////////////////
   106  //
   107  // Implementation of Composer methods not implemented by Base
   108  //
   109  ////////////////////////////////////////////////////////////////////////
   110  
   111  func (m *stackMessage) String() string {
   112  	return strings.Trim(strings.Join([]string{m.trace.String(), m.Composer.String()}, " "), " \n\t")
   113  }
   114  
   115  func (m *stackMessage) Raw() interface{} {
   116  	switch payload := m.Composer.(type) {
   117  	case *fieldMessage:
   118  		payload.fields["stack.frames"] = m.trace
   119  		return payload
   120  	default:
   121  		return StackTrace{
   122  			Context: payload,
   123  			Frames:  m.trace,
   124  		}
   125  	}
   126  }
   127  
   128  ////////////////////////////////////////////////////////////////////////
   129  //
   130  // Internal Operations for Collecting and processing data.
   131  //
   132  ////////////////////////////////////////////////////////////////////////
   133  
   134  type stackFrames []StackFrame
   135  
   136  func (f stackFrames) String() string {
   137  	out := make([]string, len(f))
   138  	for idx, frame := range f {
   139  		out[idx] = frame.String()
   140  	}
   141  
   142  	return strings.Join(out, " ")
   143  }
   144  
   145  func (f StackFrame) String() string {
   146  	if strings.HasPrefix(f.File, build.Default.GOROOT) {
   147  		// If the function's file is in the GOROOT, its format is:
   148  		// "<GOROOT>/src/<file_path>"
   149  		// Trim the "<GOROOT>/src/" prefix.
   150  		return fmt.Sprintf("%s:%d",
   151  			f.File[len(build.Default.GOROOT)+5:],
   152  			f.Line)
   153  	}
   154  
   155  	var funcName, filePath string
   156  	if funcName = f.Function[strings.LastIndex(f.Function, ".")+1:]; funcName != f.Function {
   157  		// If the function name includes the file path in it, its format will
   158  		// be:
   159  		// "<import_path>.<func_name>".
   160  		importPath := strings.TrimSuffix(f.Function, "."+funcName)
   161  		// The import path only includes the package containing the file and not
   162  		// the file name itself. Construct the file path from its import path
   163  		// and file name.
   164  		filePath = strings.Join([]string{importPath, filepath.Base(f.File)}, "/")
   165  	} else {
   166  		// If the function name does not include the import path, use the
   167  		// absolute file path as fallback.
   168  		funcName = f.Function
   169  		filePath = f.File
   170  	}
   171  
   172  	return fmt.Sprintf("%s:%d (%s)", filePath, f.Line, funcName)
   173  }
   174  
   175  func captureStack(skip int) []StackFrame {
   176  	if skip <= 0 {
   177  		// don't recorded captureStack
   178  		skip = 1
   179  	}
   180  
   181  	// captureStack is always called by a constructor, so we need
   182  	// to bump it again
   183  	skip++
   184  
   185  	trace := []StackFrame{}
   186  
   187  	for i := 0; i < maxLevels; i++ {
   188  		pc, file, line, ok := runtime.Caller(skip)
   189  		if !ok {
   190  			break
   191  		}
   192  
   193  		trace = append(trace, StackFrame{
   194  			Function: runtime.FuncForPC(pc).Name(),
   195  			File:     file,
   196  			Line:     line})
   197  
   198  		skip++
   199  	}
   200  
   201  	return trace
   202  }