package logger import ( "context" "log" "log/slog" "os" "strconv" "sync" slogtraceid "github.com/remychantenay/slog-otel" slogmulti "github.com/samber/slog-multi" "go.opentelemetry.io/contrib/bridges/otelslog" ) var ConfigPrefix = "" var textLogger *slog.Logger var otlpLogger *slog.Logger var multiLogger *slog.Logger 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 func setupStdErrHandler() slog.Handler { var programLevel = new(slog.LevelVar) // Info by default envVar := "DEBUG" if len(ConfigPrefix) > 0 { envVar = ConfigPrefix + "_" + envVar } if opt := os.Getenv(envVar); len(opt) > 0 { if debug, _ := strconv.ParseBool(opt); debug { programLevel.Set(slog.LevelDebug) } } logOptions := &slog.HandlerOptions{Level: programLevel} if len(os.Getenv("INVOCATION_ID")) > 0 { // don't add timestamps when running under systemd log.Default().SetFlags(0) logOptions.ReplaceAttr = logRemoveTime } logHandler := slogtraceid.OtelHandler{ Next: slog.NewTextHandler(os.Stderr, logOptions), } return logHandler } func setupOtlpLogger() *slog.Logger { setupOtlp.Do(func() { otlpLogger = slog.New( newLogFmtHandler(otelslog.NewHandler("common")), ) }) return otlpLogger } // 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) }) return multiLogger } // 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() } // 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) }) mu.Lock() defer mu.Unlock() if multiLogger != nil { return multiLogger } return textLogger } type loggerKey struct{} // NewContext adds the logger to the context. Use this // to for example make a request specific logger available // to other functions through the context 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, // it returns the default logger func FromContext(ctx context.Context) *slog.Logger { if l, ok := ctx.Value(loggerKey{}).(*slog.Logger); ok { return l } return Setup() } func logRemoveTime(groups []string, a slog.Attr) slog.Attr { // Remove time if a.Key == slog.TimeKey && len(groups) == 0 { return slog.Attr{} } return a }