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 }