github.com/minio/mc@v0.0.0-20240507152021-646712d5e5fb/pkg/probe/probe.go (about) 1 // Copyright (c) 2015-2021 MinIO, Inc. 2 // 3 // This file is part of MinIO Object Storage stack 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 // Package probe implements a simple mechanism to trace and return errors in large programs. 19 package probe 20 21 import ( 22 "fmt" 23 "os" 24 "path/filepath" 25 "runtime" 26 "strconv" 27 "strings" 28 "sync" 29 30 "github.com/dustin/go-humanize" 31 ) 32 33 var ( 34 // Root path to the project's source. 35 rootPath string 36 // App specific info to be included reporting. 37 appInfo map[string]string 38 ) 39 40 // Init initializes probe. It is typically called once from the main() 41 // function or at least from any source file placed at the top level 42 // source directory. 43 func Init() { 44 // Root path is automatically determined from the calling function's source file location. 45 // Catch the calling function's source file path. 46 _, file, _, _ := runtime.Caller(1) 47 // Save the directory alone. 48 rootPath = filepath.Dir(file) 49 50 appInfo = make(map[string]string) 51 } 52 53 // SetAppInfo sets app speific key:value to report additionally during call trace dump. 54 // Eg. SetAppInfo("ReleaseTag", "RELEASE_42_0") 55 // 56 // SetAppInfo("Version", "42.0") 57 // SetAppInfo("Commit", "00611fb") 58 func SetAppInfo(key, value string) { 59 appInfo[key] = value 60 } 61 62 // GetSysInfo returns useful system statistics. 63 func GetSysInfo() map[string]string { 64 host, err := os.Hostname() 65 if err != nil { 66 host = "" 67 } 68 memstats := &runtime.MemStats{} 69 runtime.ReadMemStats(memstats) 70 return map[string]string{ 71 "host.name": host, 72 "host.os": runtime.GOOS, 73 "host.arch": runtime.GOARCH, 74 "host.lang": runtime.Version(), 75 "host.cpus": strconv.Itoa(runtime.NumCPU()), 76 "mem.used": humanize.IBytes(memstats.Alloc), 77 "mem.total": humanize.IBytes(memstats.Sys), 78 "mem.heap.used": humanize.IBytes(memstats.HeapAlloc), 79 "mem.heap.total": humanize.IBytes(memstats.HeapSys), 80 } 81 } 82 83 // TracePoint container for individual trace entries in overall call trace 84 type TracePoint struct { 85 Line int `json:"line,omitempty"` 86 Filename string `json:"file,omitempty"` 87 Function string `json:"func,omitempty"` 88 Env map[string][]string `json:"env,omitempty"` 89 } 90 91 // Error implements tracing error functionality. 92 type Error struct { 93 lock sync.RWMutex 94 Cause error `json:"cause,omitempty"` 95 CallTrace []TracePoint `json:"trace,omitempty"` 96 SysInfo map[string]string `json:"sysinfo,omitempty"` 97 } 98 99 // NewError function instantiates an error probe for tracing. 100 // Default “error“ (golang's error interface) is injected in 101 // only once. Rest of the time, you trace the return path with 102 // “probe.Trace“ and finally handling them at top level 103 // 104 // Following dummy code talks about how one can pass up the 105 // errors and put them in CallTrace. 106 // 107 // func sendError() *probe.Error { 108 // return probe.NewError(errors.New("Help Needed")) 109 // } 110 // func recvError() *probe.Error { 111 // return sendError().Trace() 112 // } 113 // if err := recvError(); err != nil { 114 // log.Fatalln(err.Trace()) 115 // } 116 func NewError(e error) *Error { 117 if e == nil { 118 return nil 119 } 120 Err := Error{lock: sync.RWMutex{}, Cause: e, CallTrace: []TracePoint{}, SysInfo: GetSysInfo()} 121 return Err.trace() // Skip NewError and only instead register the NewError's caller. 122 } 123 124 // Trace records the point at which it is invoked. 125 // Stack traces are important for debugging purposes. 126 func (e *Error) Trace(fields ...string) *Error { 127 if e == nil { 128 return nil 129 } 130 131 e.lock.Lock() 132 defer e.lock.Unlock() 133 134 return e.trace(fields...) 135 } 136 137 // trace records caller's caller. It is intended for probe's own 138 // internal use. Take a look at probe.NewError for example. 139 func (e *Error) trace(fields ...string) *Error { 140 if e == nil { 141 return nil 142 } 143 pc, file, line, _ := runtime.Caller(2) 144 function := runtime.FuncForPC(pc).Name() 145 _, function = filepath.Split(function) 146 file = strings.TrimPrefix(file, rootPath+string(os.PathSeparator)) // trims project's root path. 147 var tp TracePoint 148 if len(fields) > 0 { 149 tp = TracePoint{Line: line, Filename: file, Function: function, Env: map[string][]string{"Tags": fields}} 150 } else { 151 tp = TracePoint{Line: line, Filename: file, Function: function} 152 } 153 e.CallTrace = append(e.CallTrace, tp) 154 return e 155 } 156 157 // Untrace erases last known trace entry. 158 func (e *Error) Untrace() *Error { 159 if e == nil { 160 return nil 161 } 162 e.lock.Lock() 163 defer e.lock.Unlock() 164 165 l := len(e.CallTrace) 166 if l == 0 { 167 return nil 168 } 169 e.CallTrace = e.CallTrace[:l-1] 170 return e 171 } 172 173 // ToGoError returns original error message. 174 func (e *Error) ToGoError() error { 175 if e == nil || e.Cause == nil { 176 return nil 177 } 178 return e.Cause 179 } 180 181 // String returns error message. 182 func (e *Error) String() string { 183 if e == nil || e.Cause == nil { 184 return "<nil>" 185 } 186 e.lock.RLock() 187 defer e.lock.RUnlock() 188 189 if e.Cause != nil { 190 str := e.Cause.Error() 191 callLen := len(e.CallTrace) 192 for i := callLen - 1; i >= 0; i-- { 193 if len(e.CallTrace[i].Env) > 0 { 194 str += fmt.Sprintf("\n (%d) %s:%d %s(..) Tags: [%s]", 195 i, e.CallTrace[i].Filename, e.CallTrace[i].Line, e.CallTrace[i].Function, strings.Join(e.CallTrace[i].Env["Tags"], ", ")) 196 } else { 197 str += fmt.Sprintf("\n (%d) %s:%d %s(..)", 198 i, e.CallTrace[i].Filename, e.CallTrace[i].Line, e.CallTrace[i].Function) 199 } 200 } 201 202 str += "\n " 203 204 for key, value := range appInfo { 205 str += key + ":" + value + " | " 206 } 207 208 str += "Host:" + e.SysInfo["host.name"] + " | " 209 str += "OS:" + e.SysInfo["host.os"] + " | " 210 str += "Arch:" + e.SysInfo["host.arch"] + " | " 211 str += "Lang:" + e.SysInfo["host.lang"] + " | " 212 str += "Mem:" + e.SysInfo["mem.used"] + "/" + e.SysInfo["mem.total"] + " | " 213 str += "Heap:" + e.SysInfo["mem.heap.used"] + "/" + e.SysInfo["mem.heap.total"] 214 215 return str 216 } 217 return "<nil>" 218 }