Files
zenfeed/pkg/telemetry/log/log.go
glidea 8b33df8a05 init
2025-04-19 15:50:26 +08:00

197 lines
5.0 KiB
Go

// Copyright (C) 2025 wangyusong
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package log
import (
"context"
"log/slog"
"os"
"runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
"github.com/pkg/errors"
slogdedup "github.com/veqryn/slog-dedup"
)
type Level string
const (
LevelDebug Level = "debug"
LevelInfo Level = "info"
LevelWarn Level = "warn"
LevelError Level = "error"
)
func SetLevel(level Level) error {
if level == "" {
level = LevelInfo
}
var logLevel slog.Level
switch level {
case LevelDebug:
logLevel = slog.LevelDebug
case LevelInfo:
logLevel = slog.LevelInfo
case LevelWarn:
logLevel = slog.LevelWarn
case LevelError:
logLevel = slog.LevelError
default:
return errors.Errorf("invalid log level, valid values are: %v", []Level{LevelDebug, LevelInfo, LevelWarn, LevelError})
}
newLogger := slog.New(slogdedup.NewOverwriteHandler(
slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: logLevel}),
nil,
))
mu.Lock()
defaultLogger = newLogger
mu.Unlock()
return nil
}
// With returns a new context with additional labels added to the logger.
func With(ctx context.Context, keyvals ...any) context.Context {
logger := from(ctx)
return with(ctx, logger.With(keyvals...))
}
// Debug logs a debug message with stack trace.
func Debug(ctx context.Context, msg string, args ...any) {
logWithStack(ctx, slog.LevelDebug, msg, args...)
}
// Info logs an informational message with stack trace.
func Info(ctx context.Context, msg string, args ...any) {
logWithStack(ctx, slog.LevelInfo, msg, args...)
}
// Warn logs a warning message with stack trace.
func Warn(ctx context.Context, err error, args ...any) {
logWithStack(ctx, slog.LevelWarn, err.Error(), args...)
}
// Error logs an error message with call stack trace.
func Error(ctx context.Context, err error, args ...any) {
logWithStack(ctx, slog.LevelError, err.Error(), args...)
}
// Fatal logs a fatal message with call stack trace.
// It will call os.Exit(1) after logging.
func Fatal(ctx context.Context, err error, args ...any) {
logWithStack(ctx, slog.LevelError, err.Error(), args...)
os.Exit(1)
}
type ctxKey uint8
var (
loggerCtxKey = ctxKey(0)
defaultLogger = slog.New(slogdedup.NewOverwriteHandler(slog.NewTextHandler(os.Stdout, nil), nil))
mu sync.RWMutex
// withStackLevel controls which log level and above will include stack traces.
withStackLevel atomic.Int32
)
func init() {
// Default to include stack traces for Warn and above.
SetWithStackLevel(slog.LevelWarn)
}
// SetWithStackLevel sets the minimum log level that will include stack traces.
// It should not be called in init().
func SetWithStackLevel(level slog.Level) {
withStackLevel.Store(int32(level))
}
// with returns a new context with the given logger.
func with(ctx context.Context, logger *slog.Logger) context.Context {
return context.WithValue(ctx, loggerCtxKey, logger)
}
// from retrieves the logger from context.
// Returns default logger if context has no logger.
func from(ctx context.Context) *slog.Logger {
mu.RLock()
defer mu.RUnlock()
if ctx == nil {
return defaultLogger
}
if logger, ok := ctx.Value(loggerCtxKey).(*slog.Logger); ok {
return logger
}
return defaultLogger
}
const (
stackSkip = 2 // Skip ERROR../logWithStack.
stackDepth = 5 // Maximum number of stack frames to capture.
avgFrameLen = 64
)
func logWithStack(ctx context.Context, level slog.Level, msg string, args ...any) {
logger := from(ctx)
if !logger.Enabled(ctx, level) {
// avoid to get stack trace if logging is disabled for this level
return
}
// Only include stack trace if level is >= withStackLevel
newArgs := make([]any, 0, len(args)+2)
newArgs = append(newArgs, args...)
if level >= slog.Level(withStackLevel.Load()) {
newArgs = append(newArgs, "stack", getStack(stackSkip, stackDepth))
}
logger.Log(ctx, level, msg, newArgs...)
}
// getStack returns a formatted call stack trace.
func getStack(skip, depth int) string {
pc := make([]uintptr, depth)
n := runtime.Callers(skip+2, pc) // skip itself and runtime.Callers
if n == 0 {
return ""
}
var b strings.Builder
b.Grow(n * avgFrameLen)
frames := runtime.CallersFrames(pc[:n])
first := true
for frame, more := frames.Next(); more; frame, more = frames.Next() {
if !first {
b.WriteString(" <- ")
}
first = false
b.WriteString(frame.Function)
b.WriteByte(':')
b.WriteString(strconv.Itoa(frame.Line))
}
return b.String()
}