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