mirror of
https://github.com/documize/community.git
synced 2025-07-19 13:19:43 +02:00
398 lines
11 KiB
Go
398 lines
11 KiB
Go
// Go support for leveled logs, analogous to https://github.com/google/glog.
|
|
//
|
|
// Copyright 2023 Google Inc. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package glog
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/golang/glog/internal/logsink"
|
|
)
|
|
|
|
// modulePat contains a filter for the -vmodule flag.
|
|
// It holds a verbosity level and a file pattern to match.
|
|
type modulePat struct {
|
|
pattern string
|
|
literal bool // The pattern is a literal string
|
|
full bool // The pattern wants to match the full path
|
|
level Level
|
|
}
|
|
|
|
// match reports whether the file matches the pattern. It uses a string
|
|
// comparison if the pattern contains no metacharacters.
|
|
func (m *modulePat) match(full, file string) bool {
|
|
if m.literal {
|
|
if m.full {
|
|
return full == m.pattern
|
|
}
|
|
return file == m.pattern
|
|
}
|
|
if m.full {
|
|
match, _ := filepath.Match(m.pattern, full)
|
|
return match
|
|
}
|
|
match, _ := filepath.Match(m.pattern, file)
|
|
return match
|
|
}
|
|
|
|
// isLiteral reports whether the pattern is a literal string, that is, has no metacharacters
|
|
// that require filepath.Match to be called to match the pattern.
|
|
func isLiteral(pattern string) bool {
|
|
return !strings.ContainsAny(pattern, `\*?[]`)
|
|
}
|
|
|
|
// isFull reports whether the pattern matches the full file path, that is,
|
|
// whether it contains /.
|
|
func isFull(pattern string) bool {
|
|
return strings.ContainsRune(pattern, '/')
|
|
}
|
|
|
|
// verboseFlags represents the setting of the -v and -vmodule flags.
|
|
type verboseFlags struct {
|
|
// moduleLevelCache is a sync.Map storing the -vmodule Level for each V()
|
|
// call site, identified by PC. If there is no matching -vmodule filter,
|
|
// the cached value is exactly v. moduleLevelCache is replaced with a new
|
|
// Map whenever the -vmodule or -v flag changes state.
|
|
moduleLevelCache atomic.Value
|
|
|
|
// mu guards all fields below.
|
|
mu sync.Mutex
|
|
|
|
// v stores the value of the -v flag. It may be read safely using
|
|
// sync.LoadInt32, but is only modified under mu.
|
|
v Level
|
|
|
|
// module stores the parsed -vmodule flag.
|
|
module []modulePat
|
|
|
|
// moduleLength caches len(module). If greater than zero, it
|
|
// means vmodule is enabled. It may be read safely using sync.LoadInt32, but
|
|
// is only modified under mu.
|
|
moduleLength int32
|
|
}
|
|
|
|
// NOTE: For compatibility with the open-sourced v1 version of this
|
|
// package (github.com/golang/glog) we need to retain that flag.Level
|
|
// implements the flag.Value interface. See also go/log-vs-glog.
|
|
|
|
// String is part of the flag.Value interface.
|
|
func (l *Level) String() string {
|
|
return strconv.FormatInt(int64(l.Get().(Level)), 10)
|
|
}
|
|
|
|
// Get is part of the flag.Value interface.
|
|
func (l *Level) Get() any {
|
|
if l == &vflags.v {
|
|
// l is the value registered for the -v flag.
|
|
return Level(atomic.LoadInt32((*int32)(l)))
|
|
}
|
|
return *l
|
|
}
|
|
|
|
// Set is part of the flag.Value interface.
|
|
func (l *Level) Set(value string) error {
|
|
v, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if l == &vflags.v {
|
|
// l is the value registered for the -v flag.
|
|
vflags.mu.Lock()
|
|
defer vflags.mu.Unlock()
|
|
vflags.moduleLevelCache.Store(&sync.Map{})
|
|
atomic.StoreInt32((*int32)(l), int32(v))
|
|
return nil
|
|
}
|
|
*l = Level(v)
|
|
return nil
|
|
}
|
|
|
|
// vModuleFlag is the flag.Value for the --vmodule flag.
|
|
type vModuleFlag struct{ *verboseFlags }
|
|
|
|
func (f vModuleFlag) String() string {
|
|
// Do not panic on the zero value.
|
|
// https://groups.google.com/g/golang-nuts/c/Atlr8uAjn6U/m/iId17Td5BQAJ.
|
|
if f.verboseFlags == nil {
|
|
return ""
|
|
}
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
|
|
var b bytes.Buffer
|
|
for i, f := range f.module {
|
|
if i > 0 {
|
|
b.WriteRune(',')
|
|
}
|
|
fmt.Fprintf(&b, "%s=%d", f.pattern, f.level)
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// Get returns nil for this flag type since the struct is not exported.
|
|
func (f vModuleFlag) Get() any { return nil }
|
|
|
|
var errVmoduleSyntax = errors.New("syntax error: expect comma-separated list of filename=N")
|
|
|
|
// Syntax: -vmodule=recordio=2,foo/bar/baz=1,gfs*=3
|
|
func (f vModuleFlag) Set(value string) error {
|
|
var filter []modulePat
|
|
for _, pat := range strings.Split(value, ",") {
|
|
if len(pat) == 0 {
|
|
// Empty strings such as from a trailing comma can be ignored.
|
|
continue
|
|
}
|
|
patLev := strings.Split(pat, "=")
|
|
if len(patLev) != 2 || len(patLev[0]) == 0 || len(patLev[1]) == 0 {
|
|
return errVmoduleSyntax
|
|
}
|
|
pattern := patLev[0]
|
|
v, err := strconv.Atoi(patLev[1])
|
|
if err != nil {
|
|
return errors.New("syntax error: expect comma-separated list of filename=N")
|
|
}
|
|
// TODO: check syntax of filter?
|
|
filter = append(filter, modulePat{pattern, isLiteral(pattern), isFull(pattern), Level(v)})
|
|
}
|
|
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.module = filter
|
|
atomic.StoreInt32((*int32)(&f.moduleLength), int32(len(f.module)))
|
|
f.moduleLevelCache.Store(&sync.Map{})
|
|
return nil
|
|
}
|
|
|
|
func (f *verboseFlags) levelForPC(pc uintptr) Level {
|
|
if level, ok := f.moduleLevelCache.Load().(*sync.Map).Load(pc); ok {
|
|
return level.(Level)
|
|
}
|
|
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
level := Level(f.v)
|
|
fn := runtime.FuncForPC(pc)
|
|
file, _ := fn.FileLine(pc)
|
|
// The file is something like /a/b/c/d.go. We want just the d for
|
|
// regular matches, /a/b/c/d for full matches.
|
|
file = strings.TrimSuffix(file, ".go")
|
|
full := file
|
|
if slash := strings.LastIndex(file, "/"); slash >= 0 {
|
|
file = file[slash+1:]
|
|
}
|
|
for _, filter := range f.module {
|
|
if filter.match(full, file) {
|
|
level = filter.level
|
|
break // Use the first matching level.
|
|
}
|
|
}
|
|
f.moduleLevelCache.Load().(*sync.Map).Store(pc, level)
|
|
return level
|
|
}
|
|
|
|
func (f *verboseFlags) enabled(callerDepth int, level Level) bool {
|
|
if atomic.LoadInt32(&f.moduleLength) == 0 {
|
|
// No vmodule values specified, so compare against v level.
|
|
return Level(atomic.LoadInt32((*int32)(&f.v))) >= level
|
|
}
|
|
|
|
pcs := [1]uintptr{}
|
|
if runtime.Callers(callerDepth+2, pcs[:]) < 1 {
|
|
return false
|
|
}
|
|
frame, _ := runtime.CallersFrames(pcs[:]).Next()
|
|
return f.levelForPC(frame.Entry) >= level
|
|
}
|
|
|
|
// traceLocation represents an entry in the -log_backtrace_at flag.
|
|
type traceLocation struct {
|
|
file string
|
|
line int
|
|
}
|
|
|
|
var errTraceSyntax = errors.New("syntax error: expect file.go:234")
|
|
|
|
func parseTraceLocation(value string) (traceLocation, error) {
|
|
fields := strings.Split(value, ":")
|
|
if len(fields) != 2 {
|
|
return traceLocation{}, errTraceSyntax
|
|
}
|
|
file, lineStr := fields[0], fields[1]
|
|
if !strings.Contains(file, ".") {
|
|
return traceLocation{}, errTraceSyntax
|
|
}
|
|
line, err := strconv.Atoi(lineStr)
|
|
if err != nil {
|
|
return traceLocation{}, errTraceSyntax
|
|
}
|
|
if line < 0 {
|
|
return traceLocation{}, errors.New("negative value for line")
|
|
}
|
|
return traceLocation{file, line}, nil
|
|
}
|
|
|
|
// match reports whether the specified file and line matches the trace location.
|
|
// The argument file name is the full path, not the basename specified in the flag.
|
|
func (t traceLocation) match(file string, line int) bool {
|
|
if t.line != line {
|
|
return false
|
|
}
|
|
if i := strings.LastIndex(file, "/"); i >= 0 {
|
|
file = file[i+1:]
|
|
}
|
|
return t.file == file
|
|
}
|
|
|
|
func (t traceLocation) String() string {
|
|
return fmt.Sprintf("%s:%d", t.file, t.line)
|
|
}
|
|
|
|
// traceLocations represents the -log_backtrace_at flag.
|
|
// Syntax: -log_backtrace_at=recordio.go:234,sstable.go:456
|
|
// Note that unlike vmodule the file extension is included here.
|
|
type traceLocations struct {
|
|
mu sync.Mutex
|
|
locsLen int32 // Safe for atomic read without mu.
|
|
locs []traceLocation
|
|
}
|
|
|
|
func (t *traceLocations) String() string {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
|
|
var buf bytes.Buffer
|
|
for i, tl := range t.locs {
|
|
if i > 0 {
|
|
buf.WriteString(",")
|
|
}
|
|
buf.WriteString(tl.String())
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
// Get always returns nil for this flag type since the struct is not exported
|
|
func (t *traceLocations) Get() any { return nil }
|
|
|
|
func (t *traceLocations) Set(value string) error {
|
|
var locs []traceLocation
|
|
for _, s := range strings.Split(value, ",") {
|
|
if s == "" {
|
|
continue
|
|
}
|
|
loc, err := parseTraceLocation(s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
locs = append(locs, loc)
|
|
}
|
|
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
atomic.StoreInt32(&t.locsLen, int32(len(locs)))
|
|
t.locs = locs
|
|
return nil
|
|
}
|
|
|
|
func (t *traceLocations) match(file string, line int) bool {
|
|
if atomic.LoadInt32(&t.locsLen) == 0 {
|
|
return false
|
|
}
|
|
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
for _, tl := range t.locs {
|
|
if tl.match(file, line) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// severityFlag is an atomic flag.Value implementation for logsink.Severity.
|
|
type severityFlag int32
|
|
|
|
func (s *severityFlag) get() logsink.Severity {
|
|
return logsink.Severity(atomic.LoadInt32((*int32)(s)))
|
|
}
|
|
func (s *severityFlag) String() string { return strconv.FormatInt(int64(*s), 10) }
|
|
func (s *severityFlag) Get() any { return s.get() }
|
|
func (s *severityFlag) Set(value string) error {
|
|
threshold, err := logsink.ParseSeverity(value)
|
|
if err != nil {
|
|
// Not a severity name. Try a raw number.
|
|
v, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
threshold = logsink.Severity(v)
|
|
if threshold < logsink.Info || threshold > logsink.Fatal {
|
|
return fmt.Errorf("Severity %d out of range (min %d, max %d).", v, logsink.Info, logsink.Fatal)
|
|
}
|
|
}
|
|
atomic.StoreInt32((*int32)(s), int32(threshold))
|
|
return nil
|
|
}
|
|
|
|
var (
|
|
vflags verboseFlags // The -v and -vmodule flags.
|
|
|
|
logBacktraceAt traceLocations // The -log_backtrace_at flag.
|
|
|
|
// Boolean flags. Not handled atomically because the flag.Value interface
|
|
// does not let us avoid the =true, and that shorthand is necessary for
|
|
// compatibility. TODO: does this matter enough to fix? Seems unlikely.
|
|
toStderr bool // The -logtostderr flag.
|
|
alsoToStderr bool // The -alsologtostderr flag.
|
|
|
|
stderrThreshold severityFlag // The -stderrthreshold flag.
|
|
)
|
|
|
|
// verboseEnabled returns whether the caller at the given depth should emit
|
|
// verbose logs at the given level, with depth 0 identifying the caller of
|
|
// verboseEnabled.
|
|
func verboseEnabled(callerDepth int, level Level) bool {
|
|
return vflags.enabled(callerDepth+1, level)
|
|
}
|
|
|
|
// backtraceAt returns whether the logging call at the given function and line
|
|
// should also emit a backtrace of the current call stack.
|
|
func backtraceAt(file string, line int) bool {
|
|
return logBacktraceAt.match(file, line)
|
|
}
|
|
|
|
func init() {
|
|
vflags.moduleLevelCache.Store(&sync.Map{})
|
|
|
|
flag.Var(&vflags.v, "v", "log level for V logs")
|
|
flag.Var(vModuleFlag{&vflags}, "vmodule", "comma-separated list of pattern=N settings for file-filtered logging")
|
|
|
|
flag.Var(&logBacktraceAt, "log_backtrace_at", "when logging hits line file:N, emit a stack trace")
|
|
|
|
stderrThreshold = severityFlag(logsink.Error)
|
|
|
|
flag.BoolVar(&toStderr, "logtostderr", false, "log to standard error instead of files")
|
|
flag.BoolVar(&alsoToStderr, "alsologtostderr", false, "log to standard error as well as files")
|
|
flag.Var(&stderrThreshold, "stderrthreshold", "logs at or above this threshold go to stderr")
|
|
}
|