* Fix Grafana App SDK logger log level What This commit fixes the hardcoded value of the app SDK logger log level by properly setting it during the log manager initialization. Why To prevent app SDK logging from always logging at DEBUG. Signed-off-by: Igor Suleymanov <igor.suleymanov@grafana.com> * Add missing argument to the logging test Signed-off-by: Igor Suleymanov <igor.suleymanov@grafana.com> --------- Signed-off-by: Igor Suleymanov <igor.suleymanov@grafana.com>
574 lines
15 KiB
Go
574 lines
15 KiB
Go
// Copyright 2014 The Gogs Authors. All rights reserved.
|
|
// Use of this source code is governed by a MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package log
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
gokitlog "github.com/go-kit/log"
|
|
"github.com/go-kit/log/level"
|
|
"github.com/go-stack/stack"
|
|
"github.com/mattn/go-isatty"
|
|
sloggokit "github.com/tjhop/slog-gokit"
|
|
"gopkg.in/ini.v1"
|
|
|
|
"github.com/grafana/grafana-app-sdk/logging"
|
|
"github.com/grafana/grafana/pkg/infra/log/term"
|
|
"github.com/grafana/grafana/pkg/infra/log/text"
|
|
"github.com/grafana/grafana/pkg/util"
|
|
)
|
|
|
|
var (
|
|
loggersToClose []DisposableHandler
|
|
loggersToReload []ReloadableHandler
|
|
root *logManager
|
|
now = time.Now
|
|
logTimeFormat = time.RFC3339Nano
|
|
ctxLogProviders = []ContextualLogProviderFunc{}
|
|
)
|
|
|
|
const (
|
|
// top 7 calls in the stack are within logger
|
|
DefaultCallerDepth = 7
|
|
CallerContextKey = "caller"
|
|
)
|
|
|
|
func init() {
|
|
loggersToClose = make([]DisposableHandler, 0)
|
|
loggersToReload = make([]ReloadableHandler, 0)
|
|
|
|
// Use discard by default
|
|
format := func(w io.Writer) gokitlog.Logger {
|
|
return gokitlog.NewLogfmtLogger(gokitlog.NewSyncWriter(io.Discard))
|
|
}
|
|
logger := level.NewFilter(format(os.Stderr), level.AllowInfo())
|
|
root = newManager(logger)
|
|
// Use default Info level during package initialization before config is loaded
|
|
initAppSDKLogger(logger, slog.LevelInfo)
|
|
|
|
RegisterContextualLogProvider(func(ctx context.Context) ([]any, bool) {
|
|
pFromCtx := ctx.Value(logParamsContextKey{})
|
|
if pFromCtx != nil {
|
|
return pFromCtx.([]any), true
|
|
}
|
|
return nil, false
|
|
})
|
|
}
|
|
|
|
// logManager manage loggers
|
|
type logManager struct {
|
|
*ConcreteLogger
|
|
loggersByName map[string]*ConcreteLogger
|
|
logFilters []logWithFilters
|
|
mutex sync.RWMutex
|
|
}
|
|
|
|
func newManager(logger gokitlog.Logger) *logManager {
|
|
return &logManager{
|
|
ConcreteLogger: newConcreteLogger(logger),
|
|
loggersByName: map[string]*ConcreteLogger{},
|
|
}
|
|
}
|
|
|
|
func (lm *logManager) initialize(loggers []logWithFilters, levelStr string) {
|
|
lm.mutex.Lock()
|
|
defer lm.mutex.Unlock()
|
|
|
|
defaultLoggers := make([]gokitlog.Logger, len(loggers))
|
|
for index, logger := range loggers {
|
|
defaultLoggers[index] = level.NewFilter(logger.val, logger.maxLevel)
|
|
}
|
|
|
|
lm.Swap(&compositeLogger{loggers: defaultLoggers})
|
|
lm.logFilters = loggers
|
|
|
|
loggersByName := []string{}
|
|
for k := range lm.loggersByName {
|
|
loggersByName = append(loggersByName, k)
|
|
}
|
|
sort.Strings(loggersByName)
|
|
|
|
for _, name := range loggersByName {
|
|
ctxLoggers := make([]gokitlog.Logger, len(loggers))
|
|
|
|
for index, logger := range loggers {
|
|
ctxLogger := gokitlog.With(logger.val, lm.loggersByName[name].ctx...)
|
|
if filterLevel, exists := logger.filters[name]; !exists {
|
|
ctxLoggers[index] = level.NewFilter(ctxLogger, logger.maxLevel)
|
|
} else {
|
|
ctxLoggers[index] = level.NewFilter(ctxLogger, filterLevel)
|
|
}
|
|
}
|
|
|
|
lm.loggersByName[name].Swap(&compositeLogger{loggers: ctxLoggers})
|
|
}
|
|
|
|
initAppSDKLogger(lm.ConcreteLogger, stringToSlogLevel(levelStr))
|
|
}
|
|
|
|
func (lm *logManager) New(ctx ...any) *ConcreteLogger {
|
|
// First key-value could be "logger" and a logger name, that would be handled differently
|
|
// to allow per-logger filtering. Otherwise a simple concrete logger is returned.
|
|
if len(ctx) < 2 {
|
|
return lm.ConcreteLogger
|
|
}
|
|
if ctx[0] != "logger" {
|
|
return newConcreteLogger(lm.ConcreteLogger, ctx...)
|
|
}
|
|
|
|
lm.mutex.Lock()
|
|
defer lm.mutex.Unlock()
|
|
|
|
// Logger name could be a string variable or an slog.Value()
|
|
loggerName := ""
|
|
switch v := ctx[1].(type) {
|
|
case string:
|
|
loggerName = v
|
|
case slog.Value:
|
|
loggerName = v.String()
|
|
default:
|
|
return lm.ConcreteLogger
|
|
}
|
|
|
|
if logger, exists := lm.loggersByName[loggerName]; exists {
|
|
return logger
|
|
}
|
|
|
|
if len(lm.logFilters) == 0 {
|
|
ctxLogger := newConcreteLogger(&lm.SwapLogger, ctx...)
|
|
lm.loggersByName[loggerName] = ctxLogger
|
|
return ctxLogger
|
|
}
|
|
|
|
compositeLogger := newCompositeLogger()
|
|
for _, logWithFilter := range lm.logFilters {
|
|
filterLevel, ok := logWithFilter.filters[loggerName]
|
|
if ok {
|
|
logWithFilter.val = level.NewFilter(logWithFilter.val, filterLevel)
|
|
} else {
|
|
logWithFilter.val = level.NewFilter(logWithFilter.val, logWithFilter.maxLevel)
|
|
}
|
|
|
|
compositeLogger.loggers = append(compositeLogger.loggers, logWithFilter.val)
|
|
}
|
|
|
|
ctxLogger := newConcreteLogger(compositeLogger, ctx...)
|
|
lm.loggersByName[loggerName] = ctxLogger
|
|
return ctxLogger
|
|
}
|
|
|
|
type ConcreteLogger struct {
|
|
ctx []any
|
|
gokitlog.SwapLogger
|
|
}
|
|
|
|
func newConcreteLogger(logger gokitlog.Logger, ctx ...any) *ConcreteLogger {
|
|
var swapLogger gokitlog.SwapLogger
|
|
|
|
if len(ctx) == 0 {
|
|
ctx = []any{}
|
|
swapLogger.Swap(logger)
|
|
} else {
|
|
swapLogger.Swap(gokitlog.With(logger, ctx...))
|
|
}
|
|
|
|
return &ConcreteLogger{
|
|
ctx: ctx,
|
|
SwapLogger: swapLogger,
|
|
}
|
|
}
|
|
|
|
func (cl ConcreteLogger) GetLogger() gokitlog.Logger {
|
|
return &cl.SwapLogger
|
|
}
|
|
|
|
func (cl *ConcreteLogger) Warn(msg string, args ...any) {
|
|
_ = cl.log(msg, level.WarnValue(), args...)
|
|
}
|
|
|
|
func (cl *ConcreteLogger) Debug(msg string, args ...any) {
|
|
_ = cl.log(msg, level.DebugValue(), args...)
|
|
}
|
|
|
|
func (cl *ConcreteLogger) Log(ctx ...any) error {
|
|
logger := gokitlog.With(&cl.SwapLogger, "t", gokitlog.TimestampFormat(now, logTimeFormat))
|
|
return logger.Log(ctx...)
|
|
}
|
|
|
|
func (cl *ConcreteLogger) Error(msg string, args ...any) {
|
|
_ = cl.log(msg, level.ErrorValue(), args...)
|
|
}
|
|
|
|
func (cl *ConcreteLogger) Info(msg string, args ...any) {
|
|
_ = cl.log(msg, level.InfoValue(), args...)
|
|
}
|
|
|
|
func (cl *ConcreteLogger) log(msg string, logLevel level.Value, args ...any) error {
|
|
return cl.Log(append([]any{level.Key(), logLevel, "msg", msg}, args...)...)
|
|
}
|
|
|
|
func FromContext(ctx context.Context) []any {
|
|
args := []any{}
|
|
for _, p := range ctxLogProviders {
|
|
if pArgs, exists := p(ctx); exists {
|
|
args = append(args, pArgs...)
|
|
}
|
|
}
|
|
return args
|
|
}
|
|
|
|
func (cl *ConcreteLogger) FromContext(ctx context.Context) Logger {
|
|
args := FromContext(ctx)
|
|
if len(args) > 0 {
|
|
return cl.New(args...)
|
|
}
|
|
return cl
|
|
}
|
|
|
|
func (cl *ConcreteLogger) New(ctx ...any) *ConcreteLogger {
|
|
if len(cl.ctx) == 0 {
|
|
return root.New(ctx...)
|
|
}
|
|
|
|
return newConcreteLogger(gokitlog.With(&cl.SwapLogger), ctx...)
|
|
}
|
|
|
|
// New creates a new logger.
|
|
// First ctx argument is expected to be the name of the logger.
|
|
// Note: For a contextual logger, i.e. a logger with a shared
|
|
// name plus additional contextual information, you must use the
|
|
// Logger interface New method for it to work as expected.
|
|
// Example creating a shared logger:
|
|
//
|
|
// requestLogger := log.New("request-logger")
|
|
//
|
|
// Example creating a contextual logger:
|
|
//
|
|
// contextualLogger := requestLogger.New("username", "user123")
|
|
func New(ctx ...any) *ConcreteLogger {
|
|
if len(ctx) == 0 {
|
|
return root.New()
|
|
}
|
|
|
|
ctx = append([]any{"logger"}, ctx...)
|
|
return root.New(ctx...)
|
|
}
|
|
|
|
// NewNopLogger returns a logger that doesn't do anything.
|
|
func NewNopLogger() *ConcreteLogger {
|
|
return newConcreteLogger(gokitlog.NewNopLogger())
|
|
}
|
|
|
|
func with(ctxLogger *ConcreteLogger, withFunc func(gokitlog.Logger, ...any) gokitlog.Logger, ctx []any) *ConcreteLogger {
|
|
if len(ctx) == 0 {
|
|
return ctxLogger
|
|
}
|
|
|
|
ctxLogger.Swap(withFunc(ctxLogger.GetLogger(), ctx...))
|
|
return ctxLogger
|
|
}
|
|
|
|
// WithPrefix adds context that will be added to the log message
|
|
func WithPrefix(ctxLogger *ConcreteLogger, ctx ...any) *ConcreteLogger {
|
|
return with(ctxLogger, gokitlog.WithPrefix, ctx)
|
|
}
|
|
|
|
// WithSuffix adds context that will be appended at the end of the log message
|
|
func WithSuffix(ctxLogger *ConcreteLogger, ctx ...any) *ConcreteLogger {
|
|
return with(ctxLogger, gokitlog.WithSuffix, ctx)
|
|
}
|
|
|
|
// ContextualLogProviderFunc contextual log provider function definition.
|
|
type ContextualLogProviderFunc func(ctx context.Context) ([]any, bool)
|
|
|
|
// RegisterContextualLogProvider registers a ContextualLogProviderFunc
|
|
// that will be used to provide context when Logger.FromContext is called.
|
|
func RegisterContextualLogProvider(mw ContextualLogProviderFunc) {
|
|
ctxLogProviders = append(ctxLogProviders, mw)
|
|
}
|
|
|
|
type logParamsContextKey struct{}
|
|
|
|
// WithContextualAttributes adds contextual attributes to the logger based on the given context.
|
|
// That allows loggers further down the chain to automatically log those attributes.
|
|
func WithContextualAttributes(ctx context.Context, logParams []any) context.Context {
|
|
p := logParams
|
|
if ctx.Value(logParamsContextKey{}) != nil {
|
|
p = append(ctx.Value(logParamsContextKey{}).([]any), logParams...)
|
|
}
|
|
return context.WithValue(ctx, logParamsContextKey{}, p)
|
|
}
|
|
|
|
var logLevels = map[string]level.Option{
|
|
"trace": level.AllowDebug(),
|
|
"debug": level.AllowDebug(),
|
|
"info": level.AllowInfo(),
|
|
"warn": level.AllowWarn(),
|
|
"error": level.AllowError(),
|
|
"critical": level.AllowError(),
|
|
}
|
|
|
|
func getLogLevelFromConfig(key string, defaultName string, cfg *ini.File) (string, level.Option) {
|
|
levelName := cfg.Section(key).Key("level").MustString(defaultName)
|
|
levelName = strings.ToLower(levelName)
|
|
level := getLogLevelFromString(levelName)
|
|
return levelName, level
|
|
}
|
|
|
|
func getLogLevelFromString(levelName string) level.Option {
|
|
loglevel, ok := logLevels[levelName]
|
|
|
|
if !ok {
|
|
_ = level.Error(root).Log("Unknown log level", "level", levelName)
|
|
return level.AllowError()
|
|
}
|
|
|
|
return loglevel
|
|
}
|
|
|
|
// the filter is composed with logger name and level
|
|
func getFilters(filterStrArray []string) map[string]level.Option {
|
|
filterMap := make(map[string]level.Option)
|
|
|
|
for i := 0; i < len(filterStrArray); i++ {
|
|
filterStr := strings.TrimSpace(filterStrArray[i])
|
|
|
|
if strings.HasPrefix(filterStr, ";") || strings.HasPrefix(filterStr, "#") {
|
|
if len(filterStr) == 1 {
|
|
i++
|
|
}
|
|
continue
|
|
}
|
|
|
|
parts := strings.Split(filterStr, ":")
|
|
if len(parts) > 1 {
|
|
filterMap[parts[0]] = getLogLevelFromString(parts[1])
|
|
}
|
|
}
|
|
|
|
return filterMap
|
|
}
|
|
|
|
func Stack(skip int) string {
|
|
call := stack.Caller(skip)
|
|
s := stack.Trace().TrimBelow(call).TrimRuntime()
|
|
return s.String()
|
|
}
|
|
|
|
// StackCaller returns a go-kit Valuer function that returns the stack trace from the place it is called. Argument `skip` allows skipping top n lines from the stack.
|
|
func StackCaller(skip int) gokitlog.Valuer {
|
|
return func() any {
|
|
return Stack(skip + 1)
|
|
}
|
|
}
|
|
|
|
// Caller proxies go-kit/log Caller and returns a Valuer function that returns a file and line from a specified depth
|
|
// in the callstack
|
|
func Caller(depth int) gokitlog.Valuer {
|
|
return gokitlog.Caller(depth)
|
|
}
|
|
|
|
type Formatedlogger func(w io.Writer) gokitlog.Logger
|
|
|
|
func getLogFormat(format string) Formatedlogger {
|
|
switch format {
|
|
case "console":
|
|
if isatty.IsTerminal(os.Stdout.Fd()) {
|
|
return func(w io.Writer) gokitlog.Logger {
|
|
return term.NewTerminalLogger(w)
|
|
}
|
|
}
|
|
return func(w io.Writer) gokitlog.Logger {
|
|
return text.NewTextLogger(w)
|
|
}
|
|
case "text":
|
|
return func(w io.Writer) gokitlog.Logger {
|
|
return text.NewTextLogger(w)
|
|
}
|
|
case "json":
|
|
return func(w io.Writer) gokitlog.Logger {
|
|
return gokitlog.NewJSONLogger(gokitlog.NewSyncWriter(w))
|
|
}
|
|
default:
|
|
return func(w io.Writer) gokitlog.Logger {
|
|
return text.NewTextLogger(w)
|
|
}
|
|
}
|
|
}
|
|
|
|
// this is for file logger only
|
|
func Close() error {
|
|
var err error
|
|
for _, logger := range loggersToClose {
|
|
if e := logger.Close(); e != nil && err == nil {
|
|
err = e
|
|
}
|
|
}
|
|
loggersToClose = make([]DisposableHandler, 0)
|
|
|
|
return err
|
|
}
|
|
|
|
// Reload reloads all loggers.
|
|
func Reload() error {
|
|
for _, logger := range loggersToReload {
|
|
if err := logger.Reload(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type logWithFilters struct {
|
|
val gokitlog.Logger
|
|
filters map[string]level.Option
|
|
maxLevel level.Option
|
|
}
|
|
|
|
func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) error {
|
|
if err := Close(); err != nil {
|
|
return err
|
|
}
|
|
|
|
logEnabled := cfg.Section("log").Key("enabled").MustBool(true)
|
|
if !logEnabled {
|
|
return nil
|
|
}
|
|
|
|
defaultLevelName, _ := getLogLevelFromConfig("log", "info", cfg)
|
|
defaultFilters := getFilters(util.SplitString(cfg.Section("log").Key("filters").String()))
|
|
|
|
configLoggers := make([]logWithFilters, 0, len(modes))
|
|
for _, mode := range modes {
|
|
mode = strings.TrimSpace(mode)
|
|
sec, err := cfg.GetSection("log." + mode)
|
|
if err != nil {
|
|
_ = level.Error(root).Log("Unknown log mode", "mode", mode)
|
|
return fmt.Errorf("failed to get config section log. %s: %w", mode, err)
|
|
}
|
|
|
|
// Log level.
|
|
_, leveloption := getLogLevelFromConfig("log."+mode, defaultLevelName, cfg)
|
|
modeFilters := getFilters(util.SplitString(sec.Key("filters").String()))
|
|
|
|
format := getLogFormat(sec.Key("format").MustString(""))
|
|
|
|
var handler logWithFilters
|
|
|
|
switch mode {
|
|
case "console":
|
|
handler.val = format(os.Stdout)
|
|
case "file":
|
|
fileName := sec.Key("file_name").MustString(filepath.Join(logsPath, "grafana.log"))
|
|
dpath := filepath.Dir(fileName)
|
|
if err := os.MkdirAll(dpath, 0o750); err != nil {
|
|
_ = level.Error(root).Log("Failed to create directory", "dpath", dpath, "err", err)
|
|
continue
|
|
}
|
|
fileHandler := NewFileWriter()
|
|
fileHandler.Filename = fileName
|
|
fileHandler.Format = format
|
|
fileHandler.Rotate = sec.Key("log_rotate").MustBool(true)
|
|
fileHandler.Maxlines = sec.Key("max_lines").MustInt(1000000)
|
|
fileHandler.Maxsize = 1 << uint(sec.Key("max_size_shift").MustInt(28))
|
|
fileHandler.Daily = sec.Key("daily_rotate").MustBool(true)
|
|
fileHandler.Maxdays = sec.Key("max_days").MustInt64(7)
|
|
if err := fileHandler.Init(); err != nil {
|
|
_ = level.Error(root).Log("Failed to initialize file handler", "dpath", dpath, "err", err)
|
|
continue
|
|
}
|
|
|
|
loggersToClose = append(loggersToClose, fileHandler)
|
|
loggersToReload = append(loggersToReload, fileHandler)
|
|
handler.val = fileHandler
|
|
case "syslog":
|
|
sysLogHandler := NewSyslog(sec, format)
|
|
loggersToClose = append(loggersToClose, sysLogHandler)
|
|
handler.val = sysLogHandler.logger
|
|
}
|
|
if handler.val == nil {
|
|
panic(fmt.Sprintf("Handler is uninitialized for mode %q", mode))
|
|
}
|
|
|
|
// join default filters and mode filters together
|
|
for key, value := range defaultFilters {
|
|
if _, exist := modeFilters[key]; !exist {
|
|
modeFilters[key] = value
|
|
}
|
|
}
|
|
|
|
handler.filters = modeFilters
|
|
handler.maxLevel = leveloption
|
|
|
|
configLoggers = append(configLoggers, handler)
|
|
}
|
|
if len(configLoggers) > 0 {
|
|
root.initialize(configLoggers, defaultLevelName)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetupConsoleLogger setup Grafana console logger with provided level.
|
|
func SetupConsoleLogger(level string) error {
|
|
iniFile := ini.Empty()
|
|
sLog, err := iniFile.NewSection("log")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = sLog.NewKey("level", level)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sLogConsole, err := iniFile.NewSection("log.console")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = sLogConsole.NewKey("format", "console")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = ReadLoggingConfig([]string{"console"}, "", iniFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// stringToSlogLevel converts a log level string to slog.Level
|
|
func stringToSlogLevel(levelStr string) slog.Level {
|
|
switch strings.ToLower(levelStr) {
|
|
case "trace", "debug":
|
|
return slog.LevelDebug
|
|
case "info":
|
|
return slog.LevelInfo
|
|
case "warn", "warning":
|
|
return slog.LevelWarn
|
|
case "error", "critical":
|
|
return slog.LevelError
|
|
default:
|
|
return slog.LevelInfo
|
|
}
|
|
}
|
|
|
|
func initAppSDKLogger(gkl gokitlog.Logger, level slog.Level) {
|
|
logging.DefaultLogger = logging.NewSLogLogger(sloggokit.NewGoKitHandler(gkl, level))
|
|
}
|