blob: 60c9371a0367722327ff2eaf510818928a207bb8 [file] [log] [blame]
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package log supports structured and unstructured logging with levels.
package log
import (
"context"
"fmt"
"log"
"os"
"strings"
"sync"
"golang.org/x/pkgsite/internal/experiment"
)
type Severity int
const (
SeverityDefault = Severity(iota)
SeverityDebug
SeverityInfo
SeverityWarning
SeverityError
SeverityCritical
)
func (s Severity) String() string {
switch s {
case SeverityDefault:
return "Default"
case SeverityDebug:
return "Debug"
case SeverityInfo:
return "Info"
case SeverityWarning:
return "Warning"
case SeverityError:
return "Error"
case SeverityCritical:
return "Critical"
default:
return fmt.Sprint(int(s))
}
}
type Logger interface {
Log(ctx context.Context, s Severity, payload any)
Flush()
}
var (
mu sync.Mutex
logger Logger = stdlibLogger{}
// currentLevel holds current log level.
// No logs will be printed below currentLevel.
currentLevel = SeverityDefault
)
// Set the log level
func SetLevel(v string) {
mu.Lock()
defer mu.Unlock()
currentLevel = toLevel(v)
}
func getLevel() Severity {
mu.Lock()
defer mu.Unlock()
return currentLevel
}
// stdlibLogger uses the Go standard library logger.
type stdlibLogger struct{}
func (stdlibLogger) Log(ctx context.Context, s Severity, payload any) {
var extras []string
es := experimentString(ctx)
if len(es) > 0 {
extras = append(extras, fmt.Sprintf("experiments %s", es))
}
var extra string
if len(extras) > 0 {
extra = " (" + strings.Join(extras, ", ") + ")"
}
log.Printf("%s%s: %+v", s, extra, payload)
}
func (stdlibLogger) Flush() {}
func experimentString(ctx context.Context) string {
return strings.Join(experiment.FromContext(ctx).Active(), ", ")
}
func Use(l Logger) {
mu.Lock()
defer mu.Unlock()
logger = l
}
// Infof logs a formatted string at the Info level.
func Infof(ctx context.Context, format string, args ...any) {
logf(ctx, SeverityInfo, format, args)
}
// Warningf logs a formatted string at the Warning level.
func Warningf(ctx context.Context, format string, args ...any) {
logf(ctx, SeverityWarning, format, args)
}
// Errorf logs a formatted string at the Error level.
func Errorf(ctx context.Context, format string, args ...any) {
logf(ctx, SeverityError, format, args)
}
// Debugf logs a formatted string at the Debug level.
func Debugf(ctx context.Context, format string, args ...any) {
logf(ctx, SeverityDebug, format, args)
}
// Fatalf logs formatted string at the Critical level followed by exiting the program.
func Fatalf(ctx context.Context, format string, args ...any) {
logf(ctx, SeverityCritical, format, args)
die()
}
func logf(ctx context.Context, s Severity, format string, args []any) {
doLog(ctx, s, fmt.Sprintf(format, args...))
}
// Info logs arg, which can be a string or a struct, at the Info level.
func Info(ctx context.Context, arg any) { doLog(ctx, SeverityInfo, arg) }
// Warning logs arg, which can be a string or a struct, at the Warning level.
func Warning(ctx context.Context, arg any) { doLog(ctx, SeverityWarning, arg) }
// Error logs arg, which can be a string or a struct, at the Error level.
func Error(ctx context.Context, arg any) { doLog(ctx, SeverityError, arg) }
// Debug logs arg, which can be a string or a struct, at the Debug level.
func Debug(ctx context.Context, arg any) { doLog(ctx, SeverityDebug, arg) }
// Fatal logs arg, which can be a string or a struct, at the Critical level followed by exiting the program.
func Fatal(ctx context.Context, arg any) {
doLog(ctx, SeverityCritical, arg)
die()
}
func doLog(ctx context.Context, s Severity, payload any) {
if getLevel() > s {
return
}
mu.Lock()
l := logger
mu.Unlock()
l.Log(ctx, s, payload)
}
func die() {
mu.Lock()
logger.Flush()
mu.Unlock()
os.Exit(1)
}
// toLevel returns the logging.Severity for a given string.
// Possible input values are "", "debug", "info", "warning", "error", "fatal".
// In case of invalid string input, it maps to DefaultLevel.
func toLevel(v string) Severity {
v = strings.ToLower(v)
switch v {
case "":
// default log level will print everything.
return SeverityDefault
case "debug":
return SeverityDebug
case "info":
return SeverityInfo
case "warning":
return SeverityWarning
case "error":
return SeverityError
case "fatal":
return SeverityCritical
}
// Default log level in case of invalid input.
log.Printf("Error: %s is invalid LogLevel. Possible values are [debug, info, warning, error, fatal]", v)
return SeverityDefault
}