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  }