Skip to content

feat: Use slog for logging #1898

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions cmd/monaco/deploy/internal/logging/logging.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package logging

import (
"github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/log"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/loggers"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/pkg/manifest"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/pkg/project"
)
Expand All @@ -28,9 +27,7 @@ func LogProjectsInfo(projects []project.Project) {
for _, p := range projects {
log.Info(" - %s", p)
}
if log.Level() == loggers.LevelDebug {
logConfigInfo(projects)
}
logConfigInfo(projects)
}

func logConfigInfo(projects []project.Project) {
Expand Down
2 changes: 1 addition & 1 deletion cmd/monaco/generate/deletefile/deletefile.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ func createConfigAPIEntry(c config.Config, apis api.APIs, project project.Projec

nameOfRefCfg, err := refCfgNamParamVal.ResolveValue(parameter.ResolveContext{})
if err != nil {
log.Warn("Unable to create delete entry for %s: %v", err)
log.Warn("Unable to create delete entry for %s: %v", c.Coordinate, err)
return persistence.DeleteEntry{}, err
}

Expand Down
7 changes: 4 additions & 3 deletions cmd/monaco/integrationtest/v2/supportarchive_report_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,12 @@ func TestSupportArchiveIsCreatedAsExpected(t *testing.T) {

zipReader := readZipArchive(t, fs, archive)
logFile, err := zipReader.Open(fixedTime + ".log")
require.NoError(t, err)
defer logFile.Close()
assert.NoError(t, err)

content, err := io.ReadAll(logFile)
assert.NoError(t, err)
assert.Contains(t, string(content), "debug", "expected log file to contain debug log entries")
require.NoError(t, err)
assert.Contains(t, string(content), "DEBUG", "expected log file to contain debug log entries")
})
})
}
Expand Down
3 changes: 0 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ require (
github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0
github.com/dynatrace/dynatrace-configuration-as-code-core v0.8.1-0.20250516114212-0186e259e881
github.com/go-logr/logr v1.4.2
github.com/go-logr/zapr v1.3.0
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/invopop/jsonschema v0.13.0
Expand All @@ -18,7 +17,6 @@ require (
github.com/stretchr/testify v1.10.0
github.com/wk8/go-ordered-map/v2 v2.1.8
go.uber.org/mock v0.5.2
go.uber.org/zap v1.27.0
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8
golang.org/x/oauth2 v0.30.0
gonum.org/v1/gonum v0.16.0
Expand All @@ -33,7 +31,6 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/time v0.11.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
8 changes: 0 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ github.com/dynatrace/dynatrace-configuration-as-code-core v0.8.1-0.2025051611421
github.com/dynatrace/dynatrace-configuration-as-code-core v0.8.1-0.20250516114212-0186e259e881/go.mod h1:3cRc4TbyVxH62R7GwIvvOgOoOQ4R2EnZa6wWjOD7jCQ=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
Expand Down Expand Up @@ -43,14 +41,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
Expand Down
203 changes: 145 additions & 58 deletions internal/log/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,17 @@ import (
"context"
"fmt"
"io"
"log/slog"
"os"
"path/filepath"
"strconv"
"strings"
"time"

"github.com/spf13/afero"

"github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/log/field"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/loggers"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/loggers/console"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/loggers/zap"

"github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/timeutils"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/pkg/config/coordinate"
)
Expand All @@ -38,6 +40,10 @@ const (
LogFileTimestampPrefixFormat = "20060102-150405"
)

const envVarLogFormat = "MONACO_LOG_FORMAT"
const envVarLogTime = "MONACO_LOG_TIME"
const envVarLogSource = "MONACO_LOG_SOURCE"

// CtxKeyCoord context key used for contextual coordinate information
type CtxKeyCoord struct{}

Expand All @@ -59,96 +65,186 @@ type CtxKeyAccount struct{}
// CtxValGraphComponentId context value used for correlating logs that belong to deployment of a sub graph
type CtxValGraphComponentId int

var (
_ loggers.Logger = (*zap.Logger)(nil)
_ loggers.Logger = (*console.Logger)(nil)
)
type WrappedLogger struct {
logger *slog.Logger
}

func (w *WrappedLogger) Fatal(msg string, a ...any) {
w.logger.Error(fmt.Sprintf(msg, a...))
os.Exit(1)
}

func (w *WrappedLogger) Error(msg string, a ...interface{}) {
w.logger.Error(fmt.Sprintf(msg, a...))
}

func (w *WrappedLogger) Warn(msg string, a ...interface{}) {
w.logger.Warn(fmt.Sprintf(msg, a...))
}

func (w *WrappedLogger) Info(msg string, a ...interface{}) {
w.logger.Info(fmt.Sprintf(msg, a...))
}

func (w *WrappedLogger) Debug(msg string, a ...interface{}) {
w.logger.Debug(fmt.Sprintf(msg, a...))
}

func (w *WrappedLogger) SLogger() *slog.Logger {
return w.logger
}

// WithFields adds additional [field.Field] for structured logs
// It accepts vararg fields and should not be called more than once per log call
func (w *WrappedLogger) WithFields(fields ...field.Field) *WrappedLogger {
logger := w.logger
for _, f := range fields {
logger = logger.With(f.Key, f.Value)
}
return &WrappedLogger{logger: logger}
}

func Fatal(msg string, a ...interface{}) {
std.Fatal(msg, a...)
func Fatal(msg string, a ...any) {
slog.Error(fmt.Sprintf(msg, a...))
os.Exit(1)
}

func Error(msg string, a ...interface{}) {
std.Error(msg, a...)
slog.Error(fmt.Sprintf(msg, a...))
}

func Warn(msg string, a ...interface{}) {
std.Warn(msg, a...)
slog.Warn(fmt.Sprintf(msg, a...))
}

func Info(msg string, a ...interface{}) {
std.Info(msg, a...)
slog.Info(fmt.Sprintf(msg, a...))
}

func Debug(msg string, a ...interface{}) {
std.Debug(msg, a...)
}

func Level() loggers.LogLevel {
return std.Level()
slog.Debug(fmt.Sprintf(msg, a...))
}

// WithFields adds additional [field.Field] for structured logs
// It accepts vararg fields and should not be called more than once per log call
func WithFields(fields ...field.Field) loggers.Logger {
return std.WithFields(fields...)
func WithFields(fields ...field.Field) *WrappedLogger {
return (&WrappedLogger{logger: slog.Default()}).WithFields(fields...)
}

// WithCtxFields creates a logger instance with preset structured logging [field.Field] based on the Context
// Coordinate (via [CtxKeyCoord]) and environment (via [CtxKeyEnv] [CtxValEnv]) information is added to logs from the Context
func WithCtxFields(ctx context.Context) loggers.Logger {
loggr := std
func WithCtxFields(ctx context.Context) *WrappedLogger {
f := make([]field.Field, 0, 2)
if c, ok := ctx.Value(CtxKeyCoord{}).(coordinate.Coordinate); ok {
f = append(f, field.Coordinate(c))
}
if e, ok := ctx.Value(CtxKeyEnv{}).(CtxValEnv); ok {
f = append(f, field.Environment(e.Name, e.Group))
}

if a := ctx.Value(CtxKeyAccount{}); a != nil {
if a, ok := ctx.Value(CtxKeyAccount{}).(any); ok {
f = append(f, field.F("account", a))
}

if c, ok := ctx.Value(CtxGraphComponentId{}).(CtxValGraphComponentId); ok {
f = append(f, field.F("gid", c))
}
return loggr.WithFields(f...)
return WithFields(f...)
}

var (
std loggers.Logger = console.Instance
)

func PrepareLogging(ctx context.Context, fs afero.Fs, verbose bool, loggerSpy io.Writer, fileLogging bool, enableMemstatLogging bool) {
loglevel := loggers.LevelInfo
if verbose {
loglevel = loggers.LevelDebug
}
handlers := []slog.Handler{}

var logFile, errFile afero.File
var err error
if fileLogging && fs != nil {
logFile, errFile, err = prepareLogFiles(ctx, fs, enableMemstatLogging)
logFile, errorFile, err := prepareLogFiles(ctx, fs, enableMemstatLogging)
if err != nil {
Warn("Error preparing log files: %s", err.Error())
}

if logFile != nil {
handlers = append(handlers, getHandler(logFile, getLevelFromVerbose(verbose)))
}

if errorFile != nil {
handlers = append(handlers, getHandler(errorFile, slog.LevelError))
}
}

logFormat := loggers.ParseLogFormat(os.Getenv(loggers.EnvVarLogFormat))
logTime := loggers.ParseLogTimeMode(os.Getenv(loggers.EnvVarLogTime))
if loggerSpy != nil {
handlers = append(handlers, getHandler(loggerSpy, getLevelFromVerbose(verbose)))
}

setDefaultLogger(loggers.LogOptions{
File: logFile,
ErrorFile: errFile,
JSONLogging: logFormat == loggers.LogFormatJSON,
LogLevel: loglevel,
LogSpy: loggerSpy,
LogTimeMode: logTime,
handlers = append(handlers, getHandler(os.Stderr, getLevelFromVerbose(verbose)))

var handler slog.Handler = NewTeeHandler(handlers...)
if len(handlers) == 1 {
handler = handlers[0]
}

logger := slog.New(handler)
slog.SetDefault(logger)
}

func getLevelFromVerbose(verbose bool) slog.Level {
if verbose {
return slog.LevelDebug
}

return slog.LevelInfo
}

func getHandler(w io.Writer, level slog.Leveler) slog.Handler {
if shouldUseJSON() {
return slog.NewJSONHandler(w, &slog.HandlerOptions{
AddSource: shouldAddSource(),
Level: level,
ReplaceAttr: getReplaceAttrFunc(),
})
}

return slog.NewTextHandler(w, &slog.HandlerOptions{
AddSource: shouldAddSource(),
Level: level,
ReplaceAttr: getReplaceAttrFunc(),
})
}

if err != nil {
Warn("%s", err)
func getReplaceAttrFunc() func(groups []string, a slog.Attr) slog.Attr {
useUTC := shouldUseUTC()
return func(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey && useUTC {
t := a.Value.Time()
t = t.UTC()
return slog.Attr{Key: slog.TimeKey, Value: slog.StringValue(t.Format(time.RFC3339))}
}

return a
}
}

func shouldUseJSON() bool {
v := os.Getenv(envVarLogFormat)
return strings.ToLower(v) == "json"
}

func shouldUseUTC() bool {
v := os.Getenv(envVarLogTime)
return strings.ToLower(v) == "utc"
}

func shouldAddSource() bool {
return getFeatureFlagValue(envVarLogSource, false)
}

func getFeatureFlagValue(envName string, d bool) bool {
if val, ok := os.LookupEnv(envName); ok {
value, err := strconv.ParseBool(strings.ToLower(val))
if err != nil {
return d
}
return value
}
return d
}

// LogFilePath returns the path of a logfile for the current execution time - depending on when this function is called such a file may not yet exist
func LogFilePath() string {
timestamp := timeutils.TimeAnchor().Format(LogFileTimestampPrefixFormat)
Expand Down Expand Up @@ -180,13 +276,13 @@ func prepareLogFiles(ctx context.Context, fs afero.Fs, enableMemstatLogging bool
logFilePath := LogFilePath()
logFile, err = fs.OpenFile(logFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
return nil, nil, fmt.Errorf("unable to prepare log file in %s directory: %w", LogDirectory, err)
return nil, nil, fmt.Errorf("unable to prepare log file %s: %w", logFilePath, err)
}

errFilePath := ErrorFilePath()
errFile, err = fs.OpenFile(errFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
return logFile, nil, fmt.Errorf("unable to prepare error file in %s directory: %w", LogDirectory, err)
return logFile, nil, fmt.Errorf("unable to prepare error file %s: %w", errFilePath, err)
}

if enableMemstatLogging {
Expand All @@ -199,13 +295,4 @@ func prepareLogFiles(ctx context.Context, fs afero.Fs, enableMemstatLogging bool
}

return logFile, errFile, nil

}

func setDefaultLogger(opts loggers.LogOptions) {
logger, err := zap.New(opts)
if err != nil {
panic(err)
}
std = logger
}
Loading
Loading