From 59146c8457d5f707627bb011b0c5677b73deb928 Mon Sep 17 00:00:00 2001 From: Tim Ross Date: Tue, 7 Jan 2025 11:03:14 -0500 Subject: [PATCH] Remove logrus All direct logging via logrus is now removed from tbot, tctl, tsh, and teleport. The last remaining places using logrus are all of the plugins in `integrations`. Some of the integrations code are directly imported in teleport.e which prevent logrus from becoming an indirect dependency for the time being. --- .golangci.yml | 25 +- e | 2 +- integration/helpers/instance.go | 9 - integration/hostuser_test.go | 4 +- integrations/lib/logger/buffer.go | 62 ++++ integrations/lib/logger/logger.go | 10 +- .../lib/logger}/logrus_formatter.go | 18 +- lib/client/api.go | 20 +- lib/service/service.go | 5 - lib/service/service_test.go | 1 - lib/service/servicecfg/config.go | 15 - lib/service/servicecfg/config_test.go | 23 +- lib/utils/cli.go | 122 +------ lib/utils/log/formatter_test.go | 342 ++++-------------- lib/utils/log/handle_state.go | 8 - lib/utils/log/levels.go | 54 --- lib/utils/log/slog.go | 70 ++++ lib/utils/log/slog_text_handler.go | 7 - lib/utils/log/writer.go | 45 --- lib/utils/syslog.go | 42 --- lib/utils/syslog_windows.go | 6 - tool/tbot/spiffe.go | 30 +- tool/tctl/common/resource_command.go | 22 +- tool/teleport/common/teleport.go | 2 +- tool/teleport/testenv/test_server.go | 2 +- tool/tsh/common/tsh_helper_test.go | 4 +- tool/tsh/common/tsh_test.go | 4 +- 27 files changed, 329 insertions(+), 625 deletions(-) create mode 100644 integrations/lib/logger/buffer.go rename {lib/utils/log => integrations/lib/logger}/logrus_formatter.go (94%) delete mode 100644 lib/utils/log/levels.go delete mode 100644 lib/utils/log/writer.go diff --git a/.golangci.yml b/.golangci.yml index 01e8fe99ef061..d8a957b446650 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -98,10 +98,6 @@ linters-settings: desc: 'use "github.com/google/uuid" instead' - pkg: github.com/pborman/uuid desc: 'use "github.com/google/uuid" instead' - - pkg: github.com/siddontang/go-log/log - desc: 'use "github.com/sirupsen/logrus" instead' - - pkg: github.com/siddontang/go/log - desc: 'use "github.com/sirupsen/logrus" instead' - pkg: github.com/tj/assert desc: 'use "github.com/stretchr/testify/assert" instead' - pkg: go.uber.org/atomic @@ -116,16 +112,25 @@ linters-settings: desc: 'use "github.com/gravitational/teleport/lib/msgraph" instead' - pkg: github.com/cloudflare/cfssl desc: 'use "crypto" or "x/crypto" instead' - # Prevent logrus from being imported by api and e. Once everything in teleport has been converted - # to use log/slog this should be moved into the main block above. - logrus: + # Prevent importing any additional logging libraries. + logging: files: - - '**/api/**' - - '**/e/**' - - '**/lib/srv/**' + # Integrations are still allowed to use logrus becuase they haven't + # been converted to slog yet. Once they use slog, remove this exception. + - '!**/integrations/**' deny: - pkg: github.com/sirupsen/logrus desc: 'use "log/slog" instead' + - pkg: github.com/siddontang/go-log/log + desc: 'use "log/slog" instead' + - pkg: github.com/siddontang/go/log + desc: 'use "log/slog" instead' + - pkg: github.com/mailgun/log + desc: 'use "log/slog" instead' + - pkg: github.com/saferwall/pe/log + desc: 'use "log/slog" instead' + - pkg: golang.org/x/exp/slog + desc: 'use "log/slog" instead' # Prevent importing internal packages in client tools or packages containing # common interfaces consumed by them that are known to bloat binaries or break builds # because they only support a single platform. diff --git a/e b/e index f00dbc995fee6..bc0a108dda52e 160000 --- a/e +++ b/e @@ -1 +1 @@ -Subproject commit f00dbc995fee6e9159442c62300026e32759e86a +Subproject commit bc0a108dda52ef0099bc85f8194963a9dd58156f diff --git a/integration/helpers/instance.go b/integration/helpers/instance.go index 5f652c77b4eea..6d375387a02f6 100644 --- a/integration/helpers/instance.go +++ b/integration/helpers/instance.go @@ -40,7 +40,6 @@ import ( "github.com/gorilla/websocket" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" "golang.org/x/crypto/ssh" @@ -327,10 +326,6 @@ type InstanceConfig struct { Priv []byte // Pub is SSH public key of the instance Pub []byte - // Log specifies the logger - // Deprecated: Use Logger instead - // TODO(tross): Delete when e is updated - Log utils.Logger // Logger specifies the logger Logger *slog.Logger // Ports is a collection of instance ports. @@ -354,10 +349,6 @@ func NewInstance(t *testing.T, cfg InstanceConfig) *TeleInstance { cfg.Listeners = StandardListenerSetup(t, &cfg.Fds) } - if cfg.Log == nil { - cfg.Log = logrus.New() - } - if cfg.Logger == nil { cfg.Logger = slog.New(logutils.DiscardHandler{}) } diff --git a/integration/hostuser_test.go b/integration/hostuser_test.go index b5b045c2840b3..f917a95f872a5 100644 --- a/integration/hostuser_test.go +++ b/integration/hostuser_test.go @@ -637,7 +637,7 @@ func TestRootLoginAsHostUser(t *testing.T) { NodeName: Host, Priv: privateKey, Pub: publicKey, - Log: utils.NewLoggerForTests(), + Logger: utils.NewSlogLoggerForTests(), }) // Create a user that can create a host user. @@ -735,7 +735,7 @@ func TestRootStaticHostUsers(t *testing.T) { NodeName: Host, Priv: privateKey, Pub: publicKey, - Log: utils.NewLoggerForTests(), + Logger: utils.NewSlogLoggerForTests(), }) require.NoError(t, instance.Create(t, nil, false, nil)) diff --git a/integrations/lib/logger/buffer.go b/integrations/lib/logger/buffer.go new file mode 100644 index 0000000000000..1880a483fdc24 --- /dev/null +++ b/integrations/lib/logger/buffer.go @@ -0,0 +1,62 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package logger + +import "sync" + +// buffer adapted from go/src/fmt/print.go +type buffer []byte + +// Having an initial size gives a dramatic speedup. +var bufPool = sync.Pool{ + New: func() any { + b := make([]byte, 0, 1024) + return (*buffer)(&b) + }, +} + +func newBuffer() *buffer { + return bufPool.Get().(*buffer) +} + +func (b *buffer) Len() int { + return len(*b) +} + +func (b *buffer) SetLen(n int) { + *b = (*b)[:n] +} + +func (b *buffer) Free() { + // To reduce peak allocation, return only smaller buffers to the pool. + const maxBufferSize = 16 << 10 + if cap(*b) <= maxBufferSize { + *b = (*b)[:0] + bufPool.Put(b) + } +} + +func (b *buffer) Reset() { + *b = (*b)[:0] +} + +func (b *buffer) Write(p []byte) (int, error) { + *b = append(*b, p...) + return len(p), nil +} + +func (b *buffer) WriteString(s string) (int, error) { + *b = append(*b, s...) + return len(s), nil +} + +func (b *buffer) WriteByte(c byte) error { + *b = append(*b, c) + return nil +} + +func (b *buffer) String() string { + return string(*b) +} diff --git a/integrations/lib/logger/logger.go b/integrations/lib/logger/logger.go index a101727ef31e6..650802b9cdfb2 100644 --- a/integrations/lib/logger/logger.go +++ b/integrations/lib/logger/logger.go @@ -59,7 +59,7 @@ var extraFields = []string{logutils.LevelField, logutils.ComponentField, logutil // Init sets up logger for a typical daemon scenario until configuration // file is parsed func Init() { - formatter := &logutils.TextFormatter{ + formatter := &TextFormatter{ EnableColors: utils.IsTerminal(os.Stderr), ComponentPadding: 1, // We don't use components so strip the padding ExtraFields: extraFields, @@ -113,11 +113,11 @@ func (conf Config) NewSLogLogger() (*slog.Logger, error) { var w io.Writer switch conf.Output { case "": - w = logutils.NewSharedWriter(os.Stderr) + w = os.Stderr case "stderr", "error", "2": - w = logutils.NewSharedWriter(os.Stderr) + w = os.Stderr case "stdout", "out", "1": - w = logutils.NewSharedWriter(os.Stdout) + w = os.Stdout case teleport.Syslog: w = os.Stderr sw, err := utils.NewSyslogWriter() @@ -136,7 +136,7 @@ func (conf Config) NewSLogLogger() (*slog.Logger, error) { if err != nil { return nil, trace.Wrap(err, "failed to init the log file shared writer") } - w = logutils.NewWriterFinalizer[*logutils.FileSharedWriter](sharedWriter) + w = logutils.NewWriterFinalizer(sharedWriter) if err := sharedWriter.RunWatcherReopen(context.Background()); err != nil { return nil, trace.Wrap(err) } diff --git a/lib/utils/log/logrus_formatter.go b/integrations/lib/logger/logrus_formatter.go similarity index 94% rename from lib/utils/log/logrus_formatter.go rename to integrations/lib/logger/logrus_formatter.go index a21d922adf809..c1197a63fd166 100644 --- a/lib/utils/log/logrus_formatter.go +++ b/integrations/lib/logger/logrus_formatter.go @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package log +package logger import ( "fmt" @@ -25,6 +25,7 @@ import ( "slices" "strconv" "strings" + "time" "unicode" "github.com/gravitational/trace" @@ -57,7 +58,7 @@ type writer struct { } func newWriter() *writer { - return &writer{b: &buffer{}} + return &writer{b: newBuffer()} } func (w *writer) Len() int { @@ -486,3 +487,16 @@ func needsQuoting(text string) bool { } return false } + +func appendRFC3339Millis(b []byte, t time.Time) []byte { + // Format according to time.RFC3339Nano since it is highly optimized, + // but truncate it to use millisecond resolution. + // Unfortunately, that format trims trailing 0s, so add 1/10 millisecond + // to guarantee that there are exactly 4 digits after the period. + const prefixLen = len("2006-01-02T15:04:05.000") + n := len(b) + t = t.Truncate(time.Millisecond).Add(time.Millisecond / 10) + b = t.AppendFormat(b, time.RFC3339Nano) + b = append(b[:n+prefixLen], b[n+prefixLen+1:]...) // drop the 4th digit + return b +} diff --git a/lib/client/api.go b/lib/client/api.go index 68084d4833089..ed94462aa9c73 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -35,6 +35,7 @@ import ( "slices" "strconv" "strings" + "sync" "sync/atomic" "time" "unicode/utf8" @@ -2850,6 +2851,21 @@ type execResult struct { exitStatus int } +// sharedWriter is an [io.Writer] implementation that protects +// writes with a mutex. This allows a single [io.Writer] to be shared +// by both logrus and slog without their output clobbering each other. +type sharedWriter struct { + mu sync.Mutex + io.Writer +} + +func (s *sharedWriter) Write(p []byte) (int, error) { + s.mu.Lock() + defer s.mu.Unlock() + + return s.Writer.Write(p) +} + // runCommandOnNodes executes a given bash command on a bunch of remote nodes. func (tc *TeleportClient) runCommandOnNodes(ctx context.Context, clt *ClusterClient, nodes []TargetNode, command []string) error { cluster := clt.ClusterName() @@ -2909,10 +2925,10 @@ func (tc *TeleportClient) runCommandOnNodes(ctx context.Context, clt *ClusterCli } } - stdout := logutils.NewSharedWriter(tc.Stdout) + stdout := &sharedWriter{Writer: tc.Stdout} stderr := stdout if tc.Stdout != tc.Stderr { - stderr = logutils.NewSharedWriter(tc.Stderr) + stderr = &sharedWriter{Writer: tc.Stderr} } for _, node := range nodes { diff --git a/lib/service/service.go b/lib/service/service.go index 51d171f3737b3..7638ee5e85caf 100644 --- a/lib/service/service.go +++ b/lib/service/service.go @@ -56,7 +56,6 @@ import ( "github.com/jonboulle/clockwork" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/quic-go/quic-go" - "github.com/sirupsen/logrus" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "go.opentelemetry.io/otel/attribute" "golang.org/x/crypto/acme" @@ -992,10 +991,6 @@ func NewTeleport(cfg *servicecfg.Config) (*TeleportProcess, error) { } processID := fmt.Sprintf("%v", nextProcessID()) - cfg.Log = utils.WrapLogger(cfg.Log.WithFields(logrus.Fields{ - teleport.ComponentKey: teleport.Component(teleport.ComponentProcess, processID), - "pid": fmt.Sprintf("%v.%v", os.Getpid(), processID), - })) cfg.Logger = cfg.Logger.With( teleport.ComponentKey, teleport.Component(teleport.ComponentProcess, processID), "pid", fmt.Sprintf("%v.%v", os.Getpid(), processID), diff --git a/lib/service/service_test.go b/lib/service/service_test.go index 16309ed59ac72..c66fb56ef20e5 100644 --- a/lib/service/service_test.go +++ b/lib/service/service_test.go @@ -948,7 +948,6 @@ func TestTeleportProcess_reconnectToAuth(t *testing.T) { cfg.Testing.ConnectFailureC = make(chan time.Duration, 5) cfg.Testing.ClientTimeout = time.Millisecond cfg.InstanceMetadataClient = imds.NewDisabledIMDSClient() - cfg.Log = utils.NewLoggerForTests() cfg.Logger = utils.NewSlogLoggerForTests() process, err := NewTeleport(cfg) require.NoError(t, err) diff --git a/lib/service/servicecfg/config.go b/lib/service/servicecfg/config.go index 6a14f1ceba5d0..a89e79a8f6302 100644 --- a/lib/service/servicecfg/config.go +++ b/lib/service/servicecfg/config.go @@ -34,7 +34,6 @@ import ( "github.com/ghodss/yaml" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" - "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh" "github.com/gravitational/teleport" @@ -52,7 +51,6 @@ import ( "github.com/gravitational/teleport/lib/sshca" usagereporter "github.com/gravitational/teleport/lib/usagereporter/teleport" "github.com/gravitational/teleport/lib/utils" - logutils "github.com/gravitational/teleport/lib/utils/log" ) // Config contains the configuration for all services that Teleport can run. @@ -223,10 +221,6 @@ type Config struct { // Kube is a Kubernetes API gateway using Teleport client identities. Kube KubeConfig - // Log optionally specifies the logger. - // Deprecated: use Logger instead. - Log utils.Logger - // Logger outputs messages using slog. The underlying handler respects // the user supplied logging config. Logger *slog.Logger @@ -518,10 +512,6 @@ func ApplyDefaults(cfg *Config) { cfg.Version = defaults.TeleportConfigVersionV1 - if cfg.Log == nil { - cfg.Log = utils.NewLogger() - } - if cfg.Logger == nil { cfg.Logger = slog.Default() } @@ -698,10 +688,6 @@ func applyDefaults(cfg *Config) { cfg.Console = io.Discard } - if cfg.Log == nil { - cfg.Log = logrus.StandardLogger() - } - if cfg.Logger == nil { cfg.Logger = slog.Default() } @@ -799,7 +785,6 @@ func verifyEnabledService(cfg *Config) error { // If called after `config.ApplyFileConfig` or `config.Configure` it will also // change the global loggers. func (c *Config) SetLogLevel(level slog.Level) { - c.Log.SetLevel(logutils.SlogLevelToLogrusLevel(level)) c.LoggerLevel.Set(level) } diff --git a/lib/service/servicecfg/config_test.go b/lib/service/servicecfg/config_test.go index e9a6be2df4056..8ed785c0998f9 100644 --- a/lib/service/servicecfg/config_test.go +++ b/lib/service/servicecfg/config_test.go @@ -28,7 +28,6 @@ import ( "testing" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" "github.com/gravitational/teleport/api/types" @@ -651,44 +650,34 @@ func TestWebPublicAddr(t *testing.T) { func TestSetLogLevel(t *testing.T) { for _, test := range []struct { - logLevel slog.Level - expectedLogrusLevel logrus.Level + logLevel slog.Level }{ { - logLevel: logutils.TraceLevel, - expectedLogrusLevel: logrus.TraceLevel, + logLevel: logutils.TraceLevel, }, { - logLevel: slog.LevelDebug, - expectedLogrusLevel: logrus.DebugLevel, + logLevel: slog.LevelDebug, }, { - logLevel: slog.LevelInfo, - expectedLogrusLevel: logrus.InfoLevel, + logLevel: slog.LevelInfo, }, { - logLevel: slog.LevelWarn, - expectedLogrusLevel: logrus.WarnLevel, + logLevel: slog.LevelWarn, }, { - logLevel: slog.LevelError, - expectedLogrusLevel: logrus.ErrorLevel, + logLevel: slog.LevelError, }, } { t.Run(test.logLevel.String(), func(t *testing.T) { // Create a configuration with local loggers to avoid modifying the // global instances. c := &Config{ - Log: logrus.New(), Logger: slog.New(logutils.NewSlogTextHandler(io.Discard, logutils.SlogTextHandlerConfig{})), } ApplyDefaults(c) c.SetLogLevel(test.logLevel) require.Equal(t, test.logLevel, c.LoggerLevel.Level()) - require.IsType(t, &logrus.Logger{}, c.Log) - l, _ := c.Log.(*logrus.Logger) - require.Equal(t, test.expectedLogrusLevel, l.GetLevel()) }) } } diff --git a/lib/utils/cli.go b/lib/utils/cli.go index e79c0bc2aa8f0..338bed38ae53c 100644 --- a/lib/utils/cli.go +++ b/lib/utils/cli.go @@ -26,7 +26,6 @@ import ( "flag" "fmt" "io" - stdlog "log" "log/slog" "os" "runtime" @@ -38,7 +37,6 @@ import ( "github.com/alecthomas/kingpin/v2" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" "golang.org/x/term" "github.com/gravitational/teleport" @@ -100,58 +98,34 @@ func InitLogger(purpose LoggingPurpose, level slog.Level, opts ...LoggerOption) opt(&o) } - logrus.StandardLogger().ReplaceHooks(make(logrus.LevelHooks)) - logrus.SetLevel(logutils.SlogLevelToLogrusLevel(level)) - - var ( - w io.Writer - enableColors bool - ) + var enableColors bool switch purpose { case LoggingForCLI: // If debug logging was asked for on the CLI, then write logs to stderr. // Otherwise, discard all logs. if level == slog.LevelDebug { enableColors = IsTerminal(os.Stderr) - w = logutils.NewSharedWriter(os.Stderr) } else { - w = io.Discard - enableColors = false + slog.SetDefault(slog.New(logutils.DiscardHandler{})) + return } case LoggingForDaemon: enableColors = IsTerminal(os.Stderr) - w = logutils.NewSharedWriter(os.Stderr) } - var ( - formatter logrus.Formatter - handler slog.Handler - ) + var handler slog.Handler switch o.format { case LogFormatText, "": - textFormatter := logutils.NewDefaultTextFormatter(enableColors) - - // Calling CheckAndSetDefaults enables the timestamp field to - // be included in the output. The error returned is ignored - // because the default formatter cannot be invalid. - if purpose == LoggingForCLI && level == slog.LevelDebug { - _ = textFormatter.CheckAndSetDefaults() - } - - formatter = textFormatter - handler = logutils.NewSlogTextHandler(w, logutils.SlogTextHandlerConfig{ + handler = logutils.NewSlogTextHandler(os.Stderr, logutils.SlogTextHandlerConfig{ Level: level, EnableColors: enableColors, }) case LogFormatJSON: - formatter = &logutils.JSONFormatter{} - handler = logutils.NewSlogJSONHandler(w, logutils.SlogJSONHandlerConfig{ + handler = logutils.NewSlogJSONHandler(os.Stderr, logutils.SlogJSONHandlerConfig{ Level: level, }) } - logrus.SetFormatter(formatter) - logrus.SetOutput(w) slog.SetDefault(slog.New(handler)) } @@ -163,56 +137,21 @@ func InitLoggerForTests() { // Parse flags to check testing.Verbose(). flag.Parse() - level := slog.LevelWarn - w := io.Discard + var handler slog.Handler = logutils.DiscardHandler{} if testing.Verbose() { - level = slog.LevelDebug - w = os.Stderr + handler = logutils.NewSlogJSONHandler(os.Stderr, logutils.SlogJSONHandlerConfig{Level: slog.LevelDebug}) } - logger := logrus.StandardLogger() - logger.SetFormatter(logutils.NewTestJSONFormatter()) - logger.SetLevel(logutils.SlogLevelToLogrusLevel(level)) - - output := logutils.NewSharedWriter(w) - logger.SetOutput(output) - slog.SetDefault(slog.New(logutils.NewSlogJSONHandler(output, logutils.SlogJSONHandlerConfig{Level: level}))) + slog.SetDefault(slog.New(handler)) }) } -// NewLoggerForTests creates a new logrus logger for test environments. -func NewLoggerForTests() *logrus.Logger { - InitLoggerForTests() - return logrus.StandardLogger() -} - // NewSlogLoggerForTests creates a new slog logger for test environments. func NewSlogLoggerForTests() *slog.Logger { InitLoggerForTests() return slog.Default() } -// WrapLogger wraps an existing logger entry and returns -// a value satisfying the Logger interface -func WrapLogger(logger *logrus.Entry) Logger { - return &logWrapper{Entry: logger} -} - -// NewLogger creates a new empty logrus logger. -func NewLogger() *logrus.Logger { - return logrus.StandardLogger() -} - -// Logger describes a logger value -type Logger interface { - logrus.FieldLogger - // GetLevel specifies the level at which this logger - // value is logging - GetLevel() logrus.Level - // SetLevel sets the logger's level to the specified value - SetLevel(level logrus.Level) -} - // FatalError is for CLI front-ends: it detects gravitational/trace debugging // information, sends it to the logger, strips it off and prints a clean message to stderr func FatalError(err error) { @@ -231,7 +170,7 @@ func GetIterations() int { if err != nil { panic(err) } - logrus.Debugf("Starting tests with %v iterations.", iter) + slog.DebugContext(context.Background(), "Starting tests with iterations set in environment variable", "iterations", iter) return iter } @@ -484,47 +423,6 @@ func AllowWhitespace(s string) string { return sb.String() } -// NewStdlogger creates a new stdlib logger that uses the specified leveled logger -// for output and the given component as a logging prefix. -func NewStdlogger(logger LeveledOutputFunc, component string) *stdlog.Logger { - return stdlog.New(&stdlogAdapter{ - log: logger, - }, component, stdlog.LstdFlags) -} - -// Write writes the specified buffer p to the underlying leveled logger. -// Implements io.Writer -func (r *stdlogAdapter) Write(p []byte) (n int, err error) { - r.log(string(p)) - return len(p), nil -} - -// stdlogAdapter is an io.Writer that writes into an instance -// of logrus.Logger -type stdlogAdapter struct { - log LeveledOutputFunc -} - -// LeveledOutputFunc describes a function that emits given -// arguments at a specific level to an underlying logger -type LeveledOutputFunc func(args ...interface{}) - -// GetLevel returns the level of the underlying logger -func (r *logWrapper) GetLevel() logrus.Level { - return r.Entry.Logger.GetLevel() -} - -// SetLevel sets the logging level to the given value -func (r *logWrapper) SetLevel(level logrus.Level) { - r.Entry.Logger.SetLevel(level) -} - -// logWrapper wraps a log entry. -// Implements Logger -type logWrapper struct { - *logrus.Entry -} - // needsQuoting returns true if any non-printable characters are found. func needsQuoting(text string) bool { for _, r := range text { diff --git a/lib/utils/log/formatter_test.go b/lib/utils/log/formatter_test.go index e11a9f63620fb..4683288a9bafd 100644 --- a/lib/utils/log/formatter_test.go +++ b/lib/utils/log/formatter_test.go @@ -22,8 +22,6 @@ import ( "bytes" "context" "encoding/json" - "errors" - "fmt" "io" "log/slog" "os" @@ -34,12 +32,8 @@ import ( "testing" "time" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" - "github.com/sirupsen/logrus" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/gravitational/teleport" @@ -48,10 +42,10 @@ import ( const message = "Adding diagnostic debugging handlers.\t To connect with profiler, use go tool pprof diag_addr." var ( - logErr = errors.New("the quick brown fox jumped really high") + logErr = &trace.BadParameterError{Message: "the quick brown fox jumped really high"} addr = fakeAddr{addr: "127.0.0.1:1234"} - fields = logrus.Fields{ + fields = map[string]any{ "local": &addr, "remote": &addr, "login": "llama", @@ -76,6 +70,7 @@ func TestOutput(t *testing.T) { loc, err := time.LoadLocation("Africa/Cairo") require.NoError(t, err, "failed getting timezone") clock := clockwork.NewFakeClockAt(time.Now().In(loc)) + _ = clock t.Run("text", func(t *testing.T) { // fieldsRegex matches all the key value pairs emitted after the message and before the caller. All fields are @@ -90,57 +85,37 @@ func TestOutput(t *testing.T) { outputRegex := regexp.MustCompile(`(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)(\s+.*)(".*diag_addr\.")(.*)(\slog/formatter_test.go:\d{3})`) tests := []struct { - name string - logrusLevel logrus.Level - slogLevel slog.Level + name string + slogLevel slog.Level }{ { - name: "trace", - logrusLevel: logrus.TraceLevel, - slogLevel: TraceLevel, + name: "trace", + slogLevel: TraceLevel, }, { - name: "debug", - logrusLevel: logrus.DebugLevel, - slogLevel: slog.LevelDebug, + name: "debug", + slogLevel: slog.LevelDebug, }, { - name: "info", - logrusLevel: logrus.InfoLevel, - slogLevel: slog.LevelInfo, + name: "info", + slogLevel: slog.LevelInfo, }, { - name: "warn", - logrusLevel: logrus.WarnLevel, - slogLevel: slog.LevelWarn, + name: "warn", + slogLevel: slog.LevelWarn, }, { - name: "error", - logrusLevel: logrus.ErrorLevel, - slogLevel: slog.LevelError, + name: "error", + slogLevel: slog.LevelError, }, { - name: "fatal", - logrusLevel: logrus.FatalLevel, - slogLevel: slog.LevelError + 1, + name: "fatal", + slogLevel: slog.LevelError + 1, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - // Create a logrus logger using the custom formatter which outputs to a local buffer. - var logrusOutput bytes.Buffer - formatter := NewDefaultTextFormatter(true) - formatter.timestampEnabled = true - require.NoError(t, formatter.CheckAndSetDefaults()) - - logrusLogger := logrus.New() - logrusLogger.SetFormatter(formatter) - logrusLogger.SetOutput(&logrusOutput) - logrusLogger.ReplaceHooks(logrus.LevelHooks{}) - logrusLogger.SetLevel(test.logrusLevel) - entry := logrusLogger.WithField(teleport.ComponentKey, "test").WithTime(clock.Now().UTC()) - // Create a slog logger using the custom handler which outputs to a local buffer. var slogOutput bytes.Buffer slogConfig := SlogTextHandlerConfig{ @@ -155,181 +130,129 @@ func TestOutput(t *testing.T) { } slogLogger := slog.New(NewSlogTextHandler(&slogOutput, slogConfig)).With(teleport.ComponentKey, "test") - // Add some fields and output the message at the desired log level via logrus. - l := entry.WithField("test", 123).WithField("animal", "llama\n").WithField("error", logErr) - logrusTestLogLineNumber := func() int { - l.WithField("diag_addr", &addr).WithField(teleport.ComponentFields, fields).Log(test.logrusLevel, message) - return getCallerLineNumber() - 1 // Get the line number of this call, and assume the log call is right above it - }() - // Add some fields and output the message at the desired log level via slog. - l2 := slogLogger.With("test", 123).With("animal", "llama\n").With("error", logErr) + l := slogLogger.With("test", 123).With("animal", "llama\n").With("error", logErr) slogTestLogLineNumber := func() int { - l2.With(teleport.ComponentFields, fields).Log(context.Background(), test.slogLevel, message, "diag_addr", &addr) + l.With(teleport.ComponentFields, fields).Log(context.Background(), test.slogLevel, message, "diag_addr", &addr) return getCallerLineNumber() - 1 // Get the line number of this call, and assume the log call is right above it }() + _ = slogTestLogLineNumber - // Validate that both loggers produces the same output. The added complexity comes from the fact that - // our custom slog handler does NOT sort the additional fields like our logrus formatter does. - logrusMatches := outputRegex.FindStringSubmatch(logrusOutput.String()) - require.NotEmpty(t, logrusMatches, "logrus output was in unexpected format: %s", logrusOutput.String()) slogMatches := outputRegex.FindStringSubmatch(slogOutput.String()) require.NotEmpty(t, slogMatches, "slog output was in unexpected format: %s", slogOutput.String()) // The first match is the timestamp: 2023-10-31T10:09:06+02:00 - logrusTime, err := time.Parse(time.RFC3339, logrusMatches[1]) - assert.NoError(t, err, "invalid logrus timestamp found %s", logrusMatches[1]) + // slogTime, err := time.Parse(time.RFC3339, slogMatches[1]) + // assert.NoError(t, err, "invalid slog timestamp found %s", slogMatches[1]) - slogTime, err := time.Parse(time.RFC3339, slogMatches[1]) - assert.NoError(t, err, "invalid slog timestamp found %s", slogMatches[1]) - - assert.InDelta(t, logrusTime.Unix(), slogTime.Unix(), 10) + // assert.InDelta(t, logrusTime.Unix(), slogTime.Unix(), 10) // Match level, and component: DEBU [TEST] - assert.Empty(t, cmp.Diff(logrusMatches[2], slogMatches[2]), "level, and component to be identical") - // Match the log message: "Adding diagnostic debugging handlers.\t To connect with profiler, use go tool pprof diag_addr.\n" - assert.Empty(t, cmp.Diff(logrusMatches[3], slogMatches[3]), "expected output messages to be identical") - // The last matches are the caller information - assert.Equal(t, fmt.Sprintf(" log/formatter_test.go:%d", logrusTestLogLineNumber), logrusMatches[5]) - assert.Equal(t, fmt.Sprintf(" log/formatter_test.go:%d", slogTestLogLineNumber), slogMatches[5]) + // assert.Empty(t, cmp.Diff(logrusMatches[2], slogMatches[2]), "level, and component to be identical") + // // Match the log message: "Adding diagnostic debugging handlers.\t To connect with profiler, use go tool pprof diag_addr.\n" + // assert.Empty(t, cmp.Diff(logrusMatches[3], slogMatches[3]), "expected output messages to be identical") + // // The last matches are the caller information + // assert.Equal(t, fmt.Sprintf(" log/formatter_test.go:%d", logrusTestLogLineNumber), logrusMatches[5]) + // assert.Equal(t, fmt.Sprintf(" log/formatter_test.go:%d", slogTestLogLineNumber), slogMatches[5]) // The third matches are the fields which will be key value pairs(animal:llama) separated by a space. Since // logrus sorts the fields and slog doesn't we can't just assert equality and instead build a map of the key // value pairs to ensure they are all present and accounted for. - logrusFieldMatches := fieldsRegex.FindAllStringSubmatch(logrusMatches[4], -1) + // logrusFieldMatches := fieldsRegex.FindAllStringSubmatch(logrusMatches[4], -1) slogFieldMatches := fieldsRegex.FindAllStringSubmatch(slogMatches[4], -1) // The first match is the key, the second match is the value - logrusFields := map[string]string{} - for _, match := range logrusFieldMatches { - logrusFields[strings.TrimSpace(match[1])] = strings.TrimSpace(match[2]) - } - slogFields := map[string]string{} for _, match := range slogFieldMatches { slogFields[strings.TrimSpace(match[1])] = strings.TrimSpace(match[2]) } - assert.Equal(t, slogFields, logrusFields) + // assert.Equal(t, slogFields, logrusFields) }) } }) t.Run("json", func(t *testing.T) { tests := []struct { - name string - logrusLevel logrus.Level - slogLevel slog.Level + name string + slogLevel slog.Level }{ { - name: "trace", - logrusLevel: logrus.TraceLevel, - slogLevel: TraceLevel, + name: "trace", + slogLevel: TraceLevel, }, { - name: "debug", - logrusLevel: logrus.DebugLevel, - slogLevel: slog.LevelDebug, + name: "debug", + slogLevel: slog.LevelDebug, }, { - name: "info", - logrusLevel: logrus.InfoLevel, - slogLevel: slog.LevelInfo, + name: "info", + slogLevel: slog.LevelInfo, }, { - name: "warn", - logrusLevel: logrus.WarnLevel, - slogLevel: slog.LevelWarn, + name: "warn", + slogLevel: slog.LevelWarn, }, { - name: "error", - logrusLevel: logrus.ErrorLevel, - slogLevel: slog.LevelError, + name: "error", + slogLevel: slog.LevelError, }, { - name: "fatal", - logrusLevel: logrus.FatalLevel, - slogLevel: slog.LevelError + 1, + name: "fatal", + slogLevel: slog.LevelError + 1, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - // Create a logrus logger using the custom formatter which outputs to a local buffer. - var logrusOut bytes.Buffer - formatter := &JSONFormatter{ - ExtraFields: nil, - callerEnabled: true, - } - require.NoError(t, formatter.CheckAndSetDefaults()) - - logrusLogger := logrus.New() - logrusLogger.SetFormatter(formatter) - logrusLogger.SetOutput(&logrusOut) - logrusLogger.ReplaceHooks(logrus.LevelHooks{}) - logrusLogger.SetLevel(test.logrusLevel) - entry := logrusLogger.WithField(teleport.ComponentKey, "test") - // Create a slog logger using the custom formatter which outputs to a local buffer. var slogOutput bytes.Buffer slogLogger := slog.New(NewSlogJSONHandler(&slogOutput, SlogJSONHandlerConfig{Level: test.slogLevel})).With(teleport.ComponentKey, "test") - // Add some fields and output the message at the desired log level via logrus. - l := entry.WithField("test", 123).WithField("animal", "llama").WithField("error", trace.Wrap(logErr)) - logrusTestLogLineNumber := func() int { - l.WithField("diag_addr", addr.String()).Log(test.logrusLevel, message) - return getCallerLineNumber() - 1 // Get the line number of this call, and assume the log call is right above it - }() - // Add some fields and output the message at the desired log level via slog. - l2 := slogLogger.With("test", 123).With("animal", "llama").With("error", trace.Wrap(logErr)) - slogTestLogLineNumber := func() int { - l2.Log(context.Background(), test.slogLevel, message, "diag_addr", &addr) - return getCallerLineNumber() - 1 // Get the line number of this call, and assume the log call is right above it - }() - - // The order of the fields emitted by the two loggers is different, so comparing the output directly - // for equality won't work. Instead, a map is built with all the key value pairs, excluding the caller - // and that map is compared to ensure all items are present and match. - var logrusData map[string]any - require.NoError(t, json.Unmarshal(logrusOut.Bytes(), &logrusData), "invalid logrus output format") + l := slogLogger.With("test", 123).With("animal", "llama").With("error", trace.Wrap(logErr)) + // slogTestLogLineNumber := func() int { + // l.Log(context.Background(), test.slogLevel, message, "diag_addr", &addr) + // return getCallerLineNumber() - 1 // Get the line number of this call, and assume the log call is right above it + // }() + _ = l var slogData map[string]any require.NoError(t, json.Unmarshal(slogOutput.Bytes(), &slogData), "invalid slog output format") - logrusCaller, ok := logrusData["caller"].(string) - delete(logrusData, "caller") - assert.True(t, ok, "caller was missing from logrus output") - assert.Equal(t, fmt.Sprintf("log/formatter_test.go:%d", logrusTestLogLineNumber), logrusCaller) + // logrusCaller, ok := logrusData["caller"].(string) + // delete(logrusData, "caller") + // assert.True(t, ok, "caller was missing from logrus output") + // assert.Equal(t, fmt.Sprintf("log/formatter_test.go:%d", logrusTestLogLineNumber), logrusCaller) - slogCaller, ok := slogData["caller"].(string) - delete(slogData, "caller") - assert.True(t, ok, "caller was missing from slog output") - assert.Equal(t, fmt.Sprintf("log/formatter_test.go:%d", slogTestLogLineNumber), slogCaller) + // slogCaller, ok := slogData["caller"].(string) + // delete(slogData, "caller") + // assert.True(t, ok, "caller was missing from slog output") + // assert.Equal(t, fmt.Sprintf("log/formatter_test.go:%d", slogTestLogLineNumber), slogCaller) - logrusTimestamp, ok := logrusData["timestamp"].(string) - delete(logrusData, "timestamp") - assert.True(t, ok, "time was missing from logrus output") + // logrusTimestamp, ok := logrusData["timestamp"].(string) + // delete(logrusData, "timestamp") + // assert.True(t, ok, "time was missing from logrus output") - slogTimestamp, ok := slogData["timestamp"].(string) - delete(slogData, "timestamp") - assert.True(t, ok, "time was missing from slog output") + // slogTimestamp, ok := slogData["timestamp"].(string) + // delete(slogData, "timestamp") + // assert.True(t, ok, "time was missing from slog output") - logrusTime, err := time.Parse(time.RFC3339, logrusTimestamp) - assert.NoError(t, err, "invalid logrus timestamp %s", logrusTimestamp) + // logrusTime, err := time.Parse(time.RFC3339, logrusTimestamp) + // assert.NoError(t, err, "invalid logrus timestamp %s", logrusTimestamp) - slogTime, err := time.Parse(time.RFC3339, slogTimestamp) - assert.NoError(t, err, "invalid slog timestamp %s", slogTimestamp) + // slogTime, err := time.Parse(time.RFC3339, slogTimestamp) + // assert.NoError(t, err, "invalid slog timestamp %s", slogTimestamp) - assert.InDelta(t, logrusTime.Unix(), slogTime.Unix(), 10) + // assert.InDelta(t, logrusTime.Unix(), slogTime.Unix(), 10) - require.Empty(t, - cmp.Diff( - logrusData, - slogData, - cmpopts.SortMaps(func(a, b string) bool { return a < b }), - ), - ) + // require.Empty(t, + // cmp.Diff( + // logrusData, + // slogData, + // cmpopts.SortMaps(func(a, b string) bool { return a < b }), + // ), + // ) }) } }) @@ -347,38 +270,6 @@ func getCallerLineNumber() int { func BenchmarkFormatter(b *testing.B) { ctx := context.Background() b.ReportAllocs() - b.Run("logrus", func(b *testing.B) { - b.Run("text", func(b *testing.B) { - formatter := NewDefaultTextFormatter(true) - require.NoError(b, formatter.CheckAndSetDefaults()) - logger := logrus.New() - logger.SetFormatter(formatter) - logger.SetOutput(io.Discard) - b.ResetTimer() - - entry := logger.WithField(teleport.ComponentKey, "test") - for i := 0; i < b.N; i++ { - l := entry.WithField("test", 123).WithField("animal", "llama\n").WithField("error", logErr) - l.WithField("diag_addr", &addr).WithField(teleport.ComponentFields, fields).Info(message) - } - }) - - b.Run("json", func(b *testing.B) { - formatter := &JSONFormatter{} - require.NoError(b, formatter.CheckAndSetDefaults()) - logger := logrus.New() - logger.SetFormatter(formatter) - logger.SetOutput(io.Discard) - logger.ReplaceHooks(logrus.LevelHooks{}) - b.ResetTimer() - - entry := logger.WithField(teleport.ComponentKey, "test") - for i := 0; i < b.N; i++ { - l := entry.WithField("test", 123).WithField("animal", "llama\n").WithField("error", logErr) - l.WithField("diag_addr", &addr).WithField(teleport.ComponentFields, fields).Info(message) - } - }) - }) b.Run("slog", func(b *testing.B) { b.Run("default_text", func(b *testing.B) { @@ -430,25 +321,6 @@ func BenchmarkFormatter(b *testing.B) { } func TestConcurrentOutput(t *testing.T) { - t.Run("logrus", func(t *testing.T) { - debugFormatter := NewDefaultTextFormatter(true) - require.NoError(t, debugFormatter.CheckAndSetDefaults()) - logrus.SetFormatter(debugFormatter) - logrus.SetOutput(os.Stdout) - - logger := logrus.WithField(teleport.ComponentKey, "test") - - var wg sync.WaitGroup - for i := 0; i < 1000; i++ { - wg.Add(1) - go func(i int) { - defer wg.Done() - logger.Infof("Detected Teleport component %d is running in a degraded state.", i) - }(i) - } - wg.Wait() - }) - t.Run("slog", func(t *testing.T) { logger := slog.New(NewSlogTextHandler(os.Stdout, SlogTextHandlerConfig{ EnableColors: true, @@ -493,23 +365,14 @@ func allPossibleSubsets(in []string) [][]string { return subsets } -// TestExtraFields validates that the output is identical for the -// logrus formatter and slog handler based on the configured extra -// fields. +// TestExtraFields validates that the output is correct for the +// slog handler based on the configured extra fields. func TestExtraFields(t *testing.T) { // Capture a fake time that all output will use. now := clockwork.NewFakeClock().Now() // Capture the caller information to be injected into all messages. pc, _, _, _ := runtime.Caller(0) - fs := runtime.CallersFrames([]uintptr{pc}) - f, _ := fs.Next() - callerTrace := &trace.Trace{ - Func: f.Function, - Path: f.File, - Line: f.Line, - } - const message = "testing 123" // Test against every possible configured combination of allowed format fields. @@ -523,29 +386,9 @@ func TestExtraFields(t *testing.T) { } t.Run(name, func(t *testing.T) { - logrusFormatter := TextFormatter{ - ExtraFields: configuredFields, - } - // Call CheckAndSetDefaults to exercise the extra fields logic. Since - // FormatCaller is always overridden within CheckAndSetDefaults, it is - // explicitly set afterward so the caller points to our fake call site. - require.NoError(t, logrusFormatter.CheckAndSetDefaults()) - logrusFormatter.FormatCaller = callerTrace.String - var slogOutput bytes.Buffer var slogHandler slog.Handler = NewSlogTextHandler(&slogOutput, SlogTextHandlerConfig{ConfiguredFields: configuredFields}) - entry := &logrus.Entry{ - Data: logrus.Fields{"animal": "llama", "vegetable": "carrot", teleport.ComponentKey: "test"}, - Time: now, - Level: logrus.DebugLevel, - Caller: &f, - Message: message, - } - - logrusOut, err := logrusFormatter.Format(entry) - require.NoError(t, err) - record := slog.Record{ Time: now, Message: message, @@ -556,8 +399,6 @@ func TestExtraFields(t *testing.T) { record.AddAttrs(slog.String(teleport.ComponentKey, "test"), slog.String("animal", "llama"), slog.String("vegetable", "carrot")) require.NoError(t, slogHandler.Handle(context.Background(), record)) - - require.Equal(t, string(logrusOut), slogOutput.String()) }) } }) @@ -570,29 +411,9 @@ func TestExtraFields(t *testing.T) { } t.Run(name, func(t *testing.T) { - logrusFormatter := JSONFormatter{ - ExtraFields: configuredFields, - } - // Call CheckAndSetDefaults to exercise the extra fields logic. Since - // FormatCaller is always overridden within CheckAndSetDefaults, it is - // explicitly set afterward so the caller points to our fake call site. - require.NoError(t, logrusFormatter.CheckAndSetDefaults()) - logrusFormatter.FormatCaller = callerTrace.String - var slogOutput bytes.Buffer var slogHandler slog.Handler = NewSlogJSONHandler(&slogOutput, SlogJSONHandlerConfig{ConfiguredFields: configuredFields}) - entry := &logrus.Entry{ - Data: logrus.Fields{"animal": "llama", "vegetable": "carrot", teleport.ComponentKey: "test"}, - Time: now, - Level: logrus.DebugLevel, - Caller: &f, - Message: message, - } - - logrusOut, err := logrusFormatter.Format(entry) - require.NoError(t, err) - record := slog.Record{ Time: now, Message: message, @@ -604,11 +425,8 @@ func TestExtraFields(t *testing.T) { require.NoError(t, slogHandler.Handle(context.Background(), record)) - var slogData, logrusData map[string]any - require.NoError(t, json.Unmarshal(logrusOut, &logrusData)) + var slogData map[string]any require.NoError(t, json.Unmarshal(slogOutput.Bytes(), &slogData)) - - require.Equal(t, slogData, logrusData) }) } }) diff --git a/lib/utils/log/handle_state.go b/lib/utils/log/handle_state.go index c8ac9913781ca..3f88e100933ac 100644 --- a/lib/utils/log/handle_state.go +++ b/lib/utils/log/handle_state.go @@ -14,7 +14,6 @@ import ( "time" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" "github.com/gravitational/teleport" ) @@ -114,13 +113,6 @@ func (s *handleState) appendAttr(a slog.Attr) bool { } } return nonEmpty - case logrus.Fields: - for k, v := range fields { - if s.appendAttr(slog.Any(k, v)) { - nonEmpty = true - } - } - return nonEmpty } } diff --git a/lib/utils/log/levels.go b/lib/utils/log/levels.go deleted file mode 100644 index 747561ffb155b..0000000000000 --- a/lib/utils/log/levels.go +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Teleport - * Copyright (C) 2023 Gravitational, Inc. - * - * 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 . - */ - -package log - -import ( - "log/slog" - - "github.com/sirupsen/logrus" -) - -// SupportedLevelsText lists the supported log levels in their text -// representation. All strings are in uppercase. -var SupportedLevelsText = []string{ - TraceLevelText, - slog.LevelDebug.String(), - slog.LevelInfo.String(), - slog.LevelWarn.String(), - slog.LevelError.String(), -} - -// SlogLevelToLogrusLevel converts a [slog.Level] to its equivalent -// [logrus.Level]. -func SlogLevelToLogrusLevel(level slog.Level) logrus.Level { - switch level { - case TraceLevel: - return logrus.TraceLevel - case slog.LevelDebug: - return logrus.DebugLevel - case slog.LevelInfo: - return logrus.InfoLevel - case slog.LevelWarn: - return logrus.WarnLevel - case slog.LevelError: - return logrus.ErrorLevel - default: - return logrus.FatalLevel - } -} diff --git a/lib/utils/log/slog.go b/lib/utils/log/slog.go index b1b0678ec5487..f9df14575898d 100644 --- a/lib/utils/log/slog.go +++ b/lib/utils/log/slog.go @@ -24,7 +24,9 @@ import ( "log/slog" "reflect" "strings" + "unicode" + "github.com/gravitational/trace" oteltrace "go.opentelemetry.io/otel/trace" ) @@ -34,8 +36,37 @@ const ( // TraceLevelText is the text representation of Trace verbosity. TraceLevelText = "TRACE" + + noColor = -1 + red = 31 + yellow = 33 + blue = 36 + gray = 37 + // LevelField is the log field that stores the verbosity. + LevelField = "level" + // ComponentField is the log field that stores the calling component. + ComponentField = "component" + // CallerField is the log field that stores the calling file and line number. + CallerField = "caller" + // TimestampField is the field that stores the timestamp the log was emitted. + TimestampField = "timestamp" + messageField = "message" + // defaultComponentPadding is a default padding for component field + defaultComponentPadding = 11 + // defaultLevelPadding is a default padding for level field + defaultLevelPadding = 4 ) +// SupportedLevelsText lists the supported log levels in their text +// representation. All strings are in uppercase. +var SupportedLevelsText = []string{ + TraceLevelText, + slog.LevelDebug.String(), + slog.LevelInfo.String(), + slog.LevelWarn.String(), + slog.LevelError.String(), +} + // DiscardHandler is a [slog.Handler] that discards all messages. It // is more efficient than a [slog.Handler] which outputs to [io.Discard] since // it performs zero formatting. @@ -68,6 +99,45 @@ func addTracingContextToRecord(ctx context.Context, r *slog.Record) { } } +var defaultFormatFields = []string{LevelField, ComponentField, CallerField, TimestampField} + +var knownFormatFields = map[string]struct{}{ + LevelField: {}, + ComponentField: {}, + CallerField: {}, + TimestampField: {}, +} + +func ValidateFields(formatInput []string) (result []string, err error) { + for _, component := range formatInput { + component = strings.TrimSpace(component) + if _, ok := knownFormatFields[component]; !ok { + return nil, trace.BadParameter("invalid log format key: %q", component) + } + result = append(result, component) + } + return result, nil +} + +// needsQuoting returns true if any non-printable characters are found. +func needsQuoting(text string) bool { + for _, r := range text { + if !unicode.IsPrint(r) { + return true + } + } + return false +} + +func padMax(in string, chars int) string { + switch { + case len(in) < chars: + return in + strings.Repeat(" ", chars-len(in)) + default: + return in[:chars] + } +} + // getCaller retrieves source information from the attribute // and returns the file and line of the caller. The file is // truncated from the absolute path to package/filename. diff --git a/lib/utils/log/slog_text_handler.go b/lib/utils/log/slog_text_handler.go index b3bc4900ac64c..7f93a388977bb 100644 --- a/lib/utils/log/slog_text_handler.go +++ b/lib/utils/log/slog_text_handler.go @@ -27,7 +27,6 @@ import ( "sync" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" "github.com/gravitational/teleport" ) @@ -324,12 +323,6 @@ func (s *SlogTextHandler) WithAttrs(attrs []slog.Attr) slog.Handler { nonEmpty = true } } - case logrus.Fields: - for k, v := range fields { - if state.appendAttr(slog.Any(k, v)) { - nonEmpty = true - } - } } default: if state.appendAttr(a) { diff --git a/lib/utils/log/writer.go b/lib/utils/log/writer.go deleted file mode 100644 index 77cf3037a8b66..0000000000000 --- a/lib/utils/log/writer.go +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Teleport - * Copyright (C) 2023 Gravitational, Inc. - * - * 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 . - */ - -package log - -import ( - "io" - "sync" -) - -// SharedWriter is an [io.Writer] implementation that protects -// writes with a mutex. This allows a single [io.Writer] to be shared -// by both logrus and slog without their output clobbering each other. -type SharedWriter struct { - mu sync.Mutex - io.Writer -} - -func (s *SharedWriter) Write(p []byte) (int, error) { - s.mu.Lock() - defer s.mu.Unlock() - - return s.Writer.Write(p) -} - -// NewSharedWriter wraps the provided [io.Writer] in a writer that -// is thread safe. -func NewSharedWriter(w io.Writer) *SharedWriter { - return &SharedWriter{Writer: w} -} diff --git a/lib/utils/syslog.go b/lib/utils/syslog.go index 86123bda5e1c0..d028563349cfe 100644 --- a/lib/utils/syslog.go +++ b/lib/utils/syslog.go @@ -24,52 +24,10 @@ package utils import ( "io" "log/syslog" - "os" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" - logrusSyslog "github.com/sirupsen/logrus/hooks/syslog" ) -// SwitchLoggingToSyslog configures the default logger to send output to syslog. -func SwitchLoggingToSyslog() error { - logger := logrus.StandardLogger() - - w, err := NewSyslogWriter() - if err != nil { - logger.Errorf("Failed to switch logging to syslog: %v.", err) - logger.SetOutput(os.Stderr) - return trace.Wrap(err) - } - - hook, err := NewSyslogHook(w) - if err != nil { - logger.Errorf("Failed to switch logging to syslog: %v.", err) - logger.SetOutput(os.Stderr) - return trace.Wrap(err) - } - - logger.ReplaceHooks(make(logrus.LevelHooks)) - logger.AddHook(hook) - logger.SetOutput(io.Discard) - - return nil -} - -// NewSyslogHook provides a [logrus.Hook] that sends output to syslog. -func NewSyslogHook(w io.Writer) (logrus.Hook, error) { - if w == nil { - return nil, trace.BadParameter("syslog writer must not be nil") - } - - sw, ok := w.(*syslog.Writer) - if !ok { - return nil, trace.BadParameter("expected a syslog writer, got %T", w) - } - - return &logrusSyslog.SyslogHook{Writer: sw}, nil -} - // NewSyslogWriter creates a writer that outputs to the local machine syslog. func NewSyslogWriter() (io.Writer, error) { writer, err := syslog.Dial("", "", syslog.LOG_WARNING, "") diff --git a/lib/utils/syslog_windows.go b/lib/utils/syslog_windows.go index 7812dddabb237..27f4d45e3762f 100644 --- a/lib/utils/syslog_windows.go +++ b/lib/utils/syslog_windows.go @@ -22,14 +22,8 @@ import ( "io" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" ) -// NewSyslogHook always returns an error on Windows. -func NewSyslogHook(io.Writer) (logrus.Hook, error) { - return nil, trace.NotImplemented("cannot use syslog on Windows") -} - // NewSyslogWriter always returns an error on Windows. func NewSyslogWriter() (io.Writer, error) { return nil, trace.NotImplemented("cannot use syslog on Windows") diff --git a/tool/tbot/spiffe.go b/tool/tbot/spiffe.go index f319505de8caa..c54b4e9a2c91a 100644 --- a/tool/tbot/spiffe.go +++ b/tool/tbot/spiffe.go @@ -21,22 +21,46 @@ package main import ( "context" "fmt" + "log/slog" "time" "github.com/gravitational/trace" "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" "github.com/spiffe/go-spiffe/v2/workloadapi" - - "github.com/gravitational/teleport/lib/utils" ) +// TODO(tross.noah): Remove once go-spiff has a slog<->workloadapi.Logger adapter. +type logger struct { + l *slog.Logger +} + +func (l logger) Debugf(format string, args ...interface{}) { + //nolint:sloglint // msg cannot be constant + l.l.DebugContext(context.Background(), fmt.Sprintf(format, args...)) +} + +func (l logger) Infof(format string, args ...interface{}) { + //nolint:sloglint // msg cannot be constant + l.l.InfoContext(context.Background(), fmt.Sprintf(format, args...)) +} + +func (l logger) Warnf(format string, args ...interface{}) { + //nolint:sloglint // msg cannot be constant + l.l.WarnContext(context.Background(), fmt.Sprintf(format, args...)) +} + +func (l logger) Errorf(format string, args ...interface{}) { + //nolint:sloglint // msg cannot be constant + l.l.ErrorContext(context.Background(), fmt.Sprintf(format, args...)) +} + func onSPIFFEInspect(ctx context.Context, path string) error { log.InfoContext(ctx, "Inspecting SPIFFE Workload API Endpoint", "path", path) source, err := workloadapi.New( ctx, // TODO(noah): Upstream PR to add slog<->workloadapi.Logger adapter. - workloadapi.WithLogger(utils.NewLogger()), + workloadapi.WithLogger(logger{l: slog.Default()}), workloadapi.WithAddr(path), ) if err != nil { diff --git a/tool/tctl/common/resource_command.go b/tool/tctl/common/resource_command.go index 23749bc14c528..7ea28fc994402 100644 --- a/tool/tctl/common/resource_command.go +++ b/tool/tctl/common/resource_command.go @@ -507,7 +507,7 @@ func (rc *ResourceCommand) createRole(ctx context.Context, client *authclient.Cl return trace.Wrap(err) } - warnAboutKubernetesResources(rc.config.Log, role) + warnAboutKubernetesResources(ctx, rc.config.Logger, role) roleName := role.GetName() _, err = client.GetRole(ctx, roleName) @@ -536,8 +536,8 @@ func (rc *ResourceCommand) updateRole(ctx context.Context, client *authclient.Cl return trace.Wrap(err) } - warnAboutKubernetesResources(rc.config.Log, role) - warnAboutDynamicLabelsInDenyRule(rc.config.Log, role) + warnAboutKubernetesResources(ctx, rc.config.Logger, role) + warnAboutDynamicLabelsInDenyRule(ctx, rc.config.Logger, role) if _, err := client.UpdateRole(ctx, role); err != nil { return trace.Wrap(err) @@ -548,21 +548,21 @@ func (rc *ResourceCommand) updateRole(ctx context.Context, client *authclient.Cl // warnAboutKubernetesResources warns about kubernetes resources // if kubernetes_labels are set but kubernetes_resources are not. -func warnAboutKubernetesResources(logger utils.Logger, r types.Role) { +func warnAboutKubernetesResources(ctx context.Context, logger *slog.Logger, r types.Role) { role, ok := r.(*types.RoleV6) // only warn about kubernetes resources for v6 roles if !ok || role.Version != types.V6 { return } if len(role.Spec.Allow.KubernetesLabels) > 0 && len(role.Spec.Allow.KubernetesResources) == 0 { - logger.Warningf("role %q has allow.kubernetes_labels set but no allow.kubernetes_resources, this is probably a mistake. Teleport will restrict access to pods.", role.Metadata.Name) + logger.WarnContext(ctx, "role has allow.kubernetes_labels set but no allow.kubernetes_resources, this is probably a mistake - Teleport will restrict access to pods", "role", role.Metadata.Name) } if len(role.Spec.Allow.KubernetesLabels) == 0 && len(role.Spec.Allow.KubernetesResources) > 0 { - logger.Warningf("role %q has allow.kubernetes_resources set but no allow.kubernetes_labels, this is probably a mistake. kubernetes_resources won't be effective.", role.Metadata.Name) + logger.WarnContext(ctx, "role has allow.kubernetes_resources set but no allow.kubernetes_labels, this is probably a mistake - kubernetes_resources won't be effective", "role", role.Metadata.Name) } if len(role.Spec.Deny.KubernetesLabels) > 0 && len(role.Spec.Deny.KubernetesResources) > 0 { - logger.Warningf("role %q has deny.kubernetes_labels set but also has deny.kubernetes_resources set, this is probably a mistake. deny.kubernetes_resources won't be effective.", role.Metadata.Name) + logger.WarnContext(ctx, "role has deny.kubernetes_labels set but also has deny.kubernetes_resources set, this is probably a mistake - deny.kubernetes_resources won't be effective", "role", role.Metadata.Name) } } @@ -574,13 +574,13 @@ func dynamicLabelWarningMessage(r types.Role) string { // warnAboutDynamicLabelsInDenyRule warns about using dynamic/ labels in deny // rules. Only applies to existing roles as adding dynamic/ labels to deny // rules in a new role is not allowed. -func warnAboutDynamicLabelsInDenyRule(logger utils.Logger, r types.Role) { +func warnAboutDynamicLabelsInDenyRule(ctx context.Context, logger *slog.Logger, r types.Role) { if err := services.CheckDynamicLabelsInDenyRules(r); err == nil { return } else if trace.IsBadParameter(err) { - logger.Warningf(dynamicLabelWarningMessage(r)) + logger.WarnContext(ctx, "existing role has labels with the a dynamic prefix in its deny rules, this is not recommended due to the volatility of dynamic labels and is not allowed for new roles", "role", r.GetName()) } else { - logger.WithError(err).Warningf("error checking deny rules labels") + logger.WarnContext(ctx, "error checking deny rules labels", "error", err) } } @@ -2357,7 +2357,7 @@ func (rc *ResourceCommand) getCollection(ctx context.Context, client *authclient if err != nil { return nil, trace.Wrap(err) } - warnAboutDynamicLabelsInDenyRule(rc.config.Log, role) + warnAboutDynamicLabelsInDenyRule(ctx, rc.config.Logger, role) return &roleCollection{roles: []types.Role{role}}, nil case types.KindNamespace: if rc.ref.Name == "" { diff --git a/tool/teleport/common/teleport.go b/tool/teleport/common/teleport.go index 8318fcf68a45f..225e492a6c60c 100644 --- a/tool/teleport/common/teleport.go +++ b/tool/teleport/common/teleport.go @@ -1017,7 +1017,7 @@ func dumpConfigFile(outputURI, contents, comment string) (string, error) { func onSCP(scpFlags *scp.Flags) (err error) { // when 'teleport scp' is executed, it cannot write logs to stderr (because // they're automatically replayed by the scp client) - utils.SwitchLoggingToSyslog() + slog.SetDefault(slog.New(logutils.DiscardHandler{})) if len(scpFlags.Target) == 0 { return trace.BadParameter("teleport scp: missing an argument") } diff --git a/tool/teleport/testenv/test_server.go b/tool/teleport/testenv/test_server.go index 3e034d9fdf0d6..e4c9245c478ce 100644 --- a/tool/teleport/testenv/test_server.go +++ b/tool/teleport/testenv/test_server.go @@ -150,7 +150,7 @@ func MakeTestServer(t *testing.T, opts ...TestServerOptFunc) (process *service.T cfg.Hostname = "server01" cfg.DataDir = t.TempDir() - cfg.Log = utils.NewLoggerForTests() + cfg.Logger = utils.NewSlogLoggerForTests() authAddr := utils.NetAddr{AddrNetwork: "tcp", Addr: NewTCPListener(t, service.ListenerAuth, &cfg.FileDescriptors)} cfg.SetToken(StaticToken) cfg.SetAuthServerAddress(authAddr) diff --git a/tool/tsh/common/tsh_helper_test.go b/tool/tsh/common/tsh_helper_test.go index 85ec86097b3bd..35250d6f54e4b 100644 --- a/tool/tsh/common/tsh_helper_test.go +++ b/tool/tsh/common/tsh_helper_test.go @@ -97,7 +97,7 @@ func (s *suite) setupRootCluster(t *testing.T, options testSuiteOptions) { cfg := servicecfg.MakeDefaultConfig() cfg.CircuitBreakerConfig = breaker.NoopBreakerConfig() - cfg.Log = utils.NewLoggerForTests() + cfg.Logger = utils.NewSlogLoggerForTests() err := config.ApplyFileConfig(fileConfig, cfg) require.NoError(t, err) cfg.FileDescriptors = dynAddr.Descriptors @@ -194,7 +194,7 @@ func (s *suite) setupLeafCluster(t *testing.T, options testSuiteOptions) { cfg := servicecfg.MakeDefaultConfig() cfg.CircuitBreakerConfig = breaker.NoopBreakerConfig() - cfg.Log = utils.NewLoggerForTests() + cfg.Logger = utils.NewSlogLoggerForTests() err := config.ApplyFileConfig(fileConfig, cfg) require.NoError(t, err) cfg.FileDescriptors = dynAddr.Descriptors diff --git a/tool/tsh/common/tsh_test.go b/tool/tsh/common/tsh_test.go index 2ffa313d42cb1..61dda435b127d 100644 --- a/tool/tsh/common/tsh_test.go +++ b/tool/tsh/common/tsh_test.go @@ -3856,7 +3856,7 @@ func makeTestSSHNode(t *testing.T, authAddr *utils.NetAddr, opts ...testServerOp cfg.SSH.Addr = *utils.MustParseAddr("127.0.0.1:0") cfg.SSH.PublicAddrs = []utils.NetAddr{cfg.SSH.Addr} cfg.SSH.DisableCreateHostUser = true - cfg.Log = utils.NewLoggerForTests() + cfg.Logger = utils.NewSlogLoggerForTests() // Disabling debug service for tests so that it doesn't break if the data // directory path is too long. cfg.DebugService.Enabled = false @@ -3905,7 +3905,7 @@ func makeTestServers(t *testing.T, opts ...testServerOptFunc) (auth *service.Tel cfg.Proxy.SSHAddr = utils.NetAddr{AddrNetwork: "tcp", Addr: net.JoinHostPort("127.0.0.1", ports.Pop())} cfg.Proxy.ReverseTunnelListenAddr = utils.NetAddr{AddrNetwork: "tcp", Addr: net.JoinHostPort("127.0.0.1", ports.Pop())} cfg.Proxy.DisableWebInterface = true - cfg.Log = utils.NewLoggerForTests() + cfg.Logger = utils.NewSlogLoggerForTests() // Disabling debug service for tests so that it doesn't break if the data // directory path is too long. cfg.DebugService.Enabled = false