github.com/haraldrudell/parl@v0.4.176/plog/log-instance.go (about) 1 /* 2 © 2020–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/) 3 ISC License 4 */ 5 6 // plog provides thread-safe log instances for any writer 7 package plog 8 9 import ( 10 "io" 11 "log" 12 "os" 13 "regexp" 14 "sync" 15 "sync/atomic" 16 17 "github.com/haraldrudell/parl/perrors" 18 "github.com/haraldrudell/parl/pruntime" 19 ) 20 21 const ( 22 // frames to skip in public methods 23 // - Log and Info 24 // - public method + doLog() == 2 25 // - indirectly used by Debug GetDebug GetD D 26 // - indirectly used by IsThisDebug IsThisDebugN 27 // - adapted when wrapper functions are used 28 logInstDefFrames = 2 29 // logInstDebugFrameDelta adjusts to skip only 1 frame 30 // - by adding logInstDebugFrameDelta to LogInstance.stackFramesToSkip 31 // - Used by 4 functions: Debug() GetDebug() D() GetD() 32 logInstDebugFrameDelta = -1 33 // isThisDebugDelta adjusts to skip only 1 frame 34 // - adding isThisDebugDelta to LogInstance.stackFramesToSkip. 35 // - Used by 2 functions: IsThisDebug() IsThisDebugN() 36 isThisDebugDelta = -1 37 ) 38 39 // LogInstance provide logging delegating to log.Output 40 type LogInstance struct { 41 isSilence atomic.Bool // when true, [LogInstance.Info] should not print 42 isDebug atomic.Bool // when true, debug is enabled everywhere 43 // updated by [LogInstance.SetRegexp] 44 // - used to determine function-level debug 45 infoRegexp atomic.Pointer[regexp.Regexp] 46 47 // outLock protects writer and output ensuring thread-safety 48 outLock sync.Mutex 49 // invoked by Logw printing without ensuring trailing newline 50 writer io.Writer 51 // output function for writer obtained from [log.New] 52 output func(calldepth int, s string) error 53 54 // stackFramesToSkip is used for determining debug status and to get 55 // a printable code location. 56 // stackFramesToSkip default value is 2, which is one for the invocation of 57 // Log() etc., and one for the intermediate doLog() function. 58 // Debug(), GetDebug(), D(), GetD(), IsThisDebug() and IsThisDebugN() 59 // uses stackFramesToSkip to determine whether the invoking code 60 // location has debug active. 61 62 // stackFramesToSkip defaults to logInstDefFrames = 2, which is ?. 63 // NewLogFrames(…, extraStackFramesToSkip int) allos to skip 64 // additional stack frames 65 stackFramesToSkip int 66 } 67 68 // NewLog gets a logger for Fatal and Warning for specific output 69 func NewLog(writers ...io.Writer) (lg *LogInstance) { 70 var writer io.Writer 71 if len(writers) > 0 { 72 writer = writers[0] 73 } 74 if writer == nil { 75 writer = os.Stderr 76 } 77 var logger = GetLog(writer) 78 if logger == nil { 79 logger = log.New(writer, "", 0) 80 } 81 return &LogInstance{ 82 writer: writer, 83 output: logger.Output, 84 stackFramesToSkip: logInstDefFrames, 85 } 86 } 87 88 // NewLog gets a logger for Fatal and Warning for specific output 89 func NewLogFrames(writer io.Writer, extraStackFramesToSkip int) (lg *LogInstance) { 90 if extraStackFramesToSkip < 0 { 91 panic(perrors.Errorf("newLog extraStackFramesToSkip < 0: %d", extraStackFramesToSkip)) 92 } 93 lg = NewLog(writer) 94 lg.stackFramesToSkip += extraStackFramesToSkip 95 return 96 } 97 98 // Log always prints 99 // - if debug is enabled, code location is appended 100 func (g *LogInstance) Log(format string, a ...interface{}) { 101 g.doLog(format, a...) 102 } 103 104 // Logw always prints 105 // - Logw does not ensure ending newline 106 func (g *LogInstance) Logw(format string, a ...interface{}) { 107 g.invokeWriter(Sprintf(format, a...)) 108 } 109 110 // Info prints unless silence has been configured with SetSilence(true) 111 // - IsSilent determines the state of silence 112 // - if debug is enabled, code location is appended 113 func (g *LogInstance) Info(format string, a ...interface{}) { 114 if g.isSilence.Load() { 115 return 116 } 117 g.doLog(format, a...) 118 } 119 120 // Debug outputs only if debug is configured globally or for the executing function 121 // - code location is appended 122 func (g *LogInstance) Debug(format string, a ...any) { 123 var cloc *pruntime.CodeLocation 124 if !g.isDebug.Load() { 125 regExp := g.infoRegexp.Load() 126 if regExp == nil { 127 return // debug: false regexp: nil return: noop 128 } 129 cloc = pruntime.NewCodeLocation(g.stackFramesToSkip + logInstDebugFrameDelta) 130 if !regExp.MatchString(cloc.FuncName) { 131 return // debug: false regexp: no match return: noop 132 } 133 } else { 134 cloc = pruntime.NewCodeLocation(g.stackFramesToSkip + logInstDebugFrameDelta) 135 } 136 g.invokeOutput(pruntime.AppendLocation(Sprintf(format, a...), cloc)) 137 } 138 139 // GetDebug returns a function value that can be used to invokes logging 140 // - outputs if debug is enabled for the specified caller frame 141 // - the caller frame is appended 142 // - the function value can be passed around or invoked later 143 func (g *LogInstance) GetDebug(skipFrames int) (debug func(format string, a ...any)) { 144 145 // code location appended to each log ouput 146 var cloc *pruntime.CodeLocation 147 var frameNo = g.stackFramesToSkip + logInstDebugFrameDelta + skipFrames 148 if frameNo < 1 { 149 frameNo = 1 150 } 151 cloc = pruntime.NewCodeLocation(frameNo) 152 153 // determine if printing should be carried out 154 var doPrint = g.isDebug.Load() // global debug 155 if !doPrint { 156 // check if code location is specified as debug 157 var regExp = g.infoRegexp.Load() 158 doPrint = regExp != nil && regExp.MatchString(cloc.FuncName) 159 } 160 if !doPrint { 161 return NoPrint // no debug return: no-op function 162 } 163 164 return NewOutputInvoker( 165 cloc, 166 g.invokeOutput, 167 ).Invoke 168 } 169 170 // GetD returns a function value that always invokes logging 171 // - the caller frame is appended 172 // - D is meant for temporary output intended to be removed 173 // prior to check-in 174 // - the function value can be passed around or invoked later 175 func (g *LogInstance) GetD(skipFrames int) (debug func(format string, a ...interface{})) { 176 var frameNo = g.stackFramesToSkip + logInstDebugFrameDelta + skipFrames 177 if frameNo < 1 { 178 frameNo = 1 179 } 180 181 return NewOutputInvoker( 182 pruntime.NewCodeLocation(frameNo), 183 g.invokeOutput, 184 ).Invoke 185 } 186 187 // D always prints with code location. Thread-safe 188 // - D is meant for temporary output intended to be removed 189 // prior to check-in 190 func (g *LogInstance) D(format string, a ...interface{}) { 191 g.invokeOutput( 192 pruntime.AppendLocation( 193 Sprintf(format, a...), 194 pruntime.NewCodeLocation(g.stackFramesToSkip+logInstDebugFrameDelta), 195 )) 196 } 197 198 // if SetDebug is true, Debug prints everywhere produce output 199 // - other printouts have location appended 200 // - More selective debug printing can be achieved using SetInfoRegexp 201 // that matches on function names. 202 func (g *LogInstance) SetDebug(debug bool) { 203 g.isDebug.Store(debug) 204 } 205 206 // SetRegexp defines a regular expression for function-level debug 207 // printing to stderr. 208 // - SetRegexp affects Debug() GetDebug() IsThisDebug() IsThisDebugN() 209 // functions. 210 // 211 // # Regular Expression 212 // 213 // Regular expression is the RE2 [syntax] used by golang. 214 // command-line documentation: “go doc regexp/syntax”. 215 // The regular expression is matched against code location. 216 // 217 // # Code Location Format 218 // 219 // Code location is the fully qualified function name 220 // for the executing code line being evaluated. 221 // This is a fully qualified golang package path, ".", 222 // a possible type name in parenthesis ending with "." and the function name. 223 // 224 // - method with pointer receiver: 225 // - — "github.com/haraldrudell/parl/mains.(*Executable).AddErr" 226 // - — sample regexp: mains...Executable..AddErr 227 // - top-level function: 228 // - — "github.com/haraldrudell/parl/g0.NewGoGroup" 229 // - — sample regexp: g0.NewGoGroup 230 // 231 // To obtain the fully qualified function name for a particular location: 232 // 233 // parl.Log(pruntime.NewCodeLocation(0).String()) 234 // 235 // [syntax]: https://github.com/google/re2/wiki/Syntax 236 func (g *LogInstance) SetRegexp(regExp string) (err error) { 237 238 // compile any provided regexp or vakue is nil 239 var regExpPt *regexp.Regexp 240 if regExp != "" { 241 if regExpPt, err = regexp.Compile(regExp); err != nil { 242 return perrors.Errorf("regexp.Compile: %w", err) 243 } 244 } 245 246 g.infoRegexp.Store(regExpPt) 247 248 return 249 } 250 251 // SetSilent(true) prevents Info() invocations from printing 252 func (g *LogInstance) SetSilent(silent bool) { 253 g.isSilence.Store(silent) 254 } 255 256 // IsThisDebug returns whether the executing code location 257 // has debug logging enabled 258 // - true when -debug globally enabled using SetDebug(true) 259 // - true when the -verbose regexp set with SetRegexp matches 260 func (g *LogInstance) IsThisDebug() (isDebug bool) { 261 if g.isDebug.Load() { 262 return true // global debug is on return: true 263 } 264 regExp := g.infoRegexp.Load() 265 if regExp == nil { 266 return false 267 } 268 cloc := pruntime.NewCodeLocation(g.stackFramesToSkip + isThisDebugDelta) 269 return regExp.MatchString(cloc.FuncName) 270 } 271 272 // IsThisDebugN returns whether the specified stack frame 273 // has debug logging enabled. 0 means caller of IsThisDebugN. 274 // - true when -debug globally enabled using SetDebug(true) 275 // - true when the -verbose regexp set with SetRegexp matches 276 func (g *LogInstance) IsThisDebugN(skipFrames int) (isDebug bool) { 277 if isDebug = g.isDebug.Load(); isDebug { 278 return // global debug on return: true 279 } 280 281 var regExp = g.infoRegexp.Load() 282 if regExp == nil { 283 return // no regexp return: false 284 } 285 var cloc = pruntime.NewCodeLocation(g.stackFramesToSkip + isThisDebugDelta + skipFrames) 286 isDebug = regExp.MatchString(cloc.FuncName) 287 return 288 } 289 290 // IsSilent if true it means that Info does not print 291 func (g *LogInstance) IsSilent() (isSilent bool) { 292 return g.isSilence.Load() 293 } 294 295 // invokeOutput invokes the writer’s output function with mutual exclusion 296 func (g *LogInstance) invokeOutput(s string) { 297 g.outLock.Lock() 298 defer g.outLock.Unlock() 299 300 if err := g.output(0, s); err != nil { 301 panic(perrors.Errorf("LogInstance output: %w", err)) 302 } 303 } 304 305 // invokeWriter invokes writer with mutual exclusion 306 func (g *LogInstance) invokeWriter(s string) { 307 g.outLock.Lock() 308 defer g.outLock.Unlock() 309 310 if _, err := g.writer.Write([]byte(s)); err != nil { 311 panic(perrors.Errorf("LogInstance writer: %w", err)) 312 } 313 } 314 315 // doLog invokes the writer’s output function for Log and Info 316 func (g *LogInstance) doLog(format string, a ...interface{}) { 317 s := Sprintf(format, a...) 318 if g.isDebug.Load() { 319 s = pruntime.AppendLocation(s, pruntime.NewCodeLocation(g.stackFramesToSkip)) 320 } 321 g.invokeOutput(s) 322 }