2023-07-02 02:57:36 +00:00
|
|
|
package logger
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"log"
|
2023-10-22 04:51:29 +00:00
|
|
|
"log/slog"
|
2023-07-02 02:57:36 +00:00
|
|
|
"os"
|
|
|
|
"strconv"
|
|
|
|
"sync"
|
|
|
|
|
2024-11-09 12:20:09 +00:00
|
|
|
slogtraceid "github.com/remychantenay/slog-otel"
|
|
|
|
slogmulti "github.com/samber/slog-multi"
|
|
|
|
"go.opentelemetry.io/contrib/bridges/otelslog"
|
2023-07-02 02:57:36 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var ConfigPrefix = ""
|
|
|
|
|
2024-11-09 12:20:09 +00:00
|
|
|
var textLogger *slog.Logger
|
|
|
|
var otlpLogger *slog.Logger
|
|
|
|
var multiLogger *slog.Logger
|
2023-07-02 02:57:36 +00:00
|
|
|
|
2024-11-09 12:20:09 +00:00
|
|
|
var setupText sync.Once // this sets the default
|
|
|
|
var setupOtlp sync.Once // this never sets the default
|
|
|
|
var setupMulti sync.Once // this sets the default, and will always run after the others
|
|
|
|
var mu sync.Mutex
|
2023-07-02 02:57:36 +00:00
|
|
|
|
2024-11-09 12:20:09 +00:00
|
|
|
func setupStdErrHandler() slog.Handler {
|
|
|
|
var programLevel = new(slog.LevelVar) // Info by default
|
2023-07-02 02:57:36 +00:00
|
|
|
|
2024-11-09 12:20:09 +00:00
|
|
|
envVar := "DEBUG"
|
|
|
|
if len(ConfigPrefix) > 0 {
|
|
|
|
envVar = ConfigPrefix + "_" + envVar
|
|
|
|
}
|
2023-07-02 02:57:36 +00:00
|
|
|
|
2024-11-09 12:20:09 +00:00
|
|
|
if opt := os.Getenv(envVar); len(opt) > 0 {
|
|
|
|
if debug, _ := strconv.ParseBool(opt); debug {
|
|
|
|
programLevel.Set(slog.LevelDebug)
|
2023-07-02 02:57:36 +00:00
|
|
|
}
|
2024-11-09 12:20:09 +00:00
|
|
|
}
|
2023-07-02 02:57:36 +00:00
|
|
|
|
2024-11-09 12:20:09 +00:00
|
|
|
logOptions := &slog.HandlerOptions{Level: programLevel}
|
2023-07-02 02:57:36 +00:00
|
|
|
|
2024-11-09 12:20:09 +00:00
|
|
|
if len(os.Getenv("INVOCATION_ID")) > 0 {
|
|
|
|
// don't add timestamps when running under systemd
|
|
|
|
log.Default().SetFlags(0)
|
2023-07-02 02:57:36 +00:00
|
|
|
|
2024-11-09 12:20:09 +00:00
|
|
|
logOptions.ReplaceAttr = logRemoveTime
|
|
|
|
}
|
2023-07-02 02:57:36 +00:00
|
|
|
|
2024-11-09 12:20:09 +00:00
|
|
|
logHandler := slogtraceid.OtelHandler{
|
|
|
|
Next: slog.NewTextHandler(os.Stderr, logOptions),
|
|
|
|
}
|
2023-07-02 02:57:36 +00:00
|
|
|
|
2024-11-09 12:20:09 +00:00
|
|
|
return logHandler
|
|
|
|
}
|
2023-07-02 02:57:36 +00:00
|
|
|
|
2024-11-09 12:20:09 +00:00
|
|
|
func setupOtlpLogger() *slog.Logger {
|
|
|
|
setupOtlp.Do(func() {
|
|
|
|
otlpLogger = slog.New(
|
|
|
|
newLogFmtHandler(otelslog.NewHandler("common")),
|
|
|
|
)
|
|
|
|
})
|
|
|
|
return otlpLogger
|
|
|
|
}
|
2023-07-02 02:57:36 +00:00
|
|
|
|
2024-11-09 12:20:09 +00:00
|
|
|
// SetupMultiLogger will setup and make default a logger that
|
|
|
|
// logs as described in Setup() as well as an OLTP logger.
|
|
|
|
// The "multi logger" is made the default the first time
|
|
|
|
// this function is called
|
|
|
|
func SetupMultiLogger() *slog.Logger {
|
|
|
|
setupMulti.Do(func() {
|
|
|
|
textHandler := Setup().Handler()
|
|
|
|
otlpHandler := setupOtlpLogger().Handler()
|
|
|
|
|
|
|
|
multiHandler := slogmulti.Fanout(
|
|
|
|
textHandler,
|
|
|
|
otlpHandler,
|
|
|
|
)
|
|
|
|
mu.Lock()
|
|
|
|
defer mu.Unlock()
|
|
|
|
multiLogger = slog.New(multiHandler)
|
|
|
|
slog.SetDefault(multiLogger)
|
|
|
|
})
|
2023-07-02 02:57:36 +00:00
|
|
|
|
2024-11-09 12:20:09 +00:00
|
|
|
return multiLogger
|
|
|
|
}
|
2023-07-02 02:57:36 +00:00
|
|
|
|
2024-11-09 12:20:09 +00:00
|
|
|
// SetupOLTP configures and returns a logger sending logs
|
|
|
|
// via OpenTelemetry (configured via the tracing package).
|
|
|
|
//
|
|
|
|
// This was made to work with Loki + Grafana that makes it
|
|
|
|
// hard to view the log attributes in the UI, so the log
|
|
|
|
// message is formatted similarly to the text logger. The
|
|
|
|
// attributes are duplicated as OLTP attributes in the
|
|
|
|
// log messages. https://github.com/grafana/loki/issues/14788
|
|
|
|
func SetupOLTP() *slog.Logger {
|
|
|
|
return setupOtlpLogger()
|
|
|
|
}
|
2023-07-02 02:57:36 +00:00
|
|
|
|
2024-11-09 12:20:09 +00:00
|
|
|
// Setup returns an slog.Logger configured for text formatting
|
|
|
|
// to stderr.
|
|
|
|
// OpenTelemetry trace_id and span_id's are logged as attributes
|
|
|
|
// when available.
|
|
|
|
// When the application is running under systemd timestamps are
|
|
|
|
// omitted. On first call the slog default logger is set to this
|
|
|
|
// logger as well.
|
|
|
|
//
|
|
|
|
// If SetupMultiLogger has been called Setup() will return
|
|
|
|
// the "multi logger"
|
|
|
|
func Setup() *slog.Logger {
|
|
|
|
setupText.Do(func() {
|
|
|
|
h := setupStdErrHandler()
|
|
|
|
textLogger = slog.New(h)
|
|
|
|
slog.SetDefault(textLogger)
|
2023-07-02 02:57:36 +00:00
|
|
|
})
|
|
|
|
|
2024-11-09 12:20:09 +00:00
|
|
|
mu.Lock()
|
|
|
|
defer mu.Unlock()
|
|
|
|
|
|
|
|
if multiLogger != nil {
|
|
|
|
return multiLogger
|
|
|
|
}
|
|
|
|
return textLogger
|
2023-07-02 02:57:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type loggerKey struct{}
|
|
|
|
|
2024-11-09 12:20:09 +00:00
|
|
|
// NewContext adds the logger to the context. Use this
|
|
|
|
// to for example make a request specific logger available
|
|
|
|
// to other functions through the context
|
2023-07-02 02:57:36 +00:00
|
|
|
func NewContext(ctx context.Context, l *slog.Logger) context.Context {
|
|
|
|
return context.WithValue(ctx, loggerKey{}, l)
|
|
|
|
}
|
|
|
|
|
|
|
|
// FromContext retrieves a logger from the context. If there is none,
|
2024-11-09 12:20:09 +00:00
|
|
|
// it returns the default logger
|
2023-07-02 02:57:36 +00:00
|
|
|
func FromContext(ctx context.Context) *slog.Logger {
|
|
|
|
if l, ok := ctx.Value(loggerKey{}).(*slog.Logger); ok {
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
return Setup()
|
|
|
|
}
|
2024-11-09 12:20:09 +00:00
|
|
|
|
|
|
|
func logRemoveTime(groups []string, a slog.Attr) slog.Attr {
|
|
|
|
// Remove time
|
|
|
|
if a.Key == slog.TimeKey && len(groups) == 0 {
|
|
|
|
return slog.Attr{}
|
|
|
|
}
|
|
|
|
return a
|
|
|
|
}
|