| // 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") |
| } |