TUN-7127: Disconnect logger level requirement for management

By default, we want streaming logs to be able to stream debug logs
from cloudflared without needing to update the remote cloudflared's
configuration. This disconnects the provided local log level sent
to console, file, etc. from the level that management tunnel will
utilize via requested filters.
This commit is contained in:
Devin Carr 2023-04-06 11:31:47 -07:00
parent 820a201603
commit 55ed995bf0
3 changed files with 98 additions and 36 deletions

View File

@ -64,17 +64,36 @@ func fallbackLogger(err error) *zerolog.Logger {
return &failLog return &failLog
} }
type resilientMultiWriter struct { // resilientMultiWriter is an alternative to zerolog's so that we can make it resilient to individual
writers []io.Writer
}
// This custom resilientMultiWriter is an alternative to zerolog's so that we can make it resilient to individual
// writer's errors. E.g., when running as a Windows service, the console writer fails, but we don't want to // writer's errors. E.g., when running as a Windows service, the console writer fails, but we don't want to
// allow that to prevent all logging to fail due to breaking the for loop upon an error. // allow that to prevent all logging to fail due to breaking the for loop upon an error.
type resilientMultiWriter struct {
level zerolog.Level
writers []io.Writer
managementWriter zerolog.LevelWriter
}
func (t resilientMultiWriter) Write(p []byte) (n int, err error) { func (t resilientMultiWriter) Write(p []byte) (n int, err error) {
for _, w := range t.writers { for _, w := range t.writers {
_, _ = w.Write(p) _, _ = w.Write(p)
} }
if t.managementWriter != nil {
_, _ = t.managementWriter.Write(p)
}
return len(p), nil
}
func (t resilientMultiWriter) WriteLevel(level zerolog.Level, p []byte) (n int, err error) {
// Only write the event to normal writers if it exceeds the level, but always write to the
// management logger and let it decided with the provided level of the log event.
if t.level <= level {
for _, w := range t.writers {
_, _ = w.Write(p)
}
}
if t.managementWriter != nil {
_, _ = t.managementWriter.WriteLevel(level, p)
}
return len(p), nil return len(p), nil
} }
@ -105,17 +124,18 @@ func newZerolog(loggerConfig *Config) *zerolog.Logger {
writers = append(writers, rollingLogger) writers = append(writers, rollingLogger)
} }
var managementWriter zerolog.LevelWriter
if features.Contains(features.FeatureManagementLogs) { if features.Contains(features.FeatureManagementLogs) {
writers = append(writers, ManagementLogger) managementWriter = ManagementLogger
} }
multi := resilientMultiWriter{writers}
level, levelErr := zerolog.ParseLevel(loggerConfig.MinLevel) level, levelErr := zerolog.ParseLevel(loggerConfig.MinLevel)
if levelErr != nil { if levelErr != nil {
level = zerolog.InfoLevel level = zerolog.InfoLevel
} }
log := zerolog.New(multi).With().Timestamp().Logger().Level(level)
multi := resilientMultiWriter{level, writers, managementWriter}
log := zerolog.New(multi).With().Timestamp().Logger()
if !levelErrorLogged && levelErr != nil { if !levelErrorLogged && levelErr != nil {
log.Error().Msgf("Failed to parse log level %q, using %q instead", loggerConfig.MinLevel, level) log.Error().Msgf("Failed to parse log level %q, using %q instead", loggerConfig.MinLevel, level)
levelErrorLogged = true levelErrorLogged = true

View File

@ -9,14 +9,13 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
var writeCalls int
type mockedWriter struct { type mockedWriter struct {
wantErr bool wantErr bool
writeCalls int
} }
func (c mockedWriter) Write(p []byte) (int, error) { func (c *mockedWriter) Write(p []byte) (int, error) {
writeCalls++ c.writeCalls++
if c.wantErr { if c.wantErr {
return -1, errors.New("Expected error") return -1, errors.New("Expected error")
@ -26,65 +25,108 @@ func (c mockedWriter) Write(p []byte) (int, error) {
} }
// Tests that a new writer is only used if it actually works. // Tests that a new writer is only used if it actually works.
func TestResilientMultiWriter(t *testing.T) { func TestResilientMultiWriter_Errors(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
writers []io.Writer writers []*mockedWriter
}{ }{
{ {
name: "All valid writers", name: "All valid writers",
writers: []io.Writer{ writers: []*mockedWriter{
mockedWriter{ {
wantErr: false, wantErr: false,
}, },
mockedWriter{ {
wantErr: false, wantErr: false,
}, },
}, },
}, },
{ {
name: "All invalid writers", name: "All invalid writers",
writers: []io.Writer{ writers: []*mockedWriter{
mockedWriter{ {
wantErr: true, wantErr: true,
}, },
mockedWriter{ {
wantErr: true, wantErr: true,
}, },
}, },
}, },
{ {
name: "First invalid writer", name: "First invalid writer",
writers: []io.Writer{ writers: []*mockedWriter{
mockedWriter{ {
wantErr: true, wantErr: true,
}, },
mockedWriter{ {
wantErr: false, wantErr: false,
}, },
}, },
}, },
{ {
name: "First valid writer", name: "First valid writer",
writers: []io.Writer{ writers: []*mockedWriter{
mockedWriter{ {
wantErr: false, wantErr: false,
}, },
mockedWriter{ {
wantErr: true, wantErr: true,
}, },
}, },
}, },
} }
for _, tt := range tests { for _, test := range tests {
writers := tt.writers t.Run(test.name, func(t *testing.T) {
multiWriter := resilientMultiWriter{writers} writers := []io.Writer{}
for _, w := range test.writers {
writers = append(writers, w)
}
multiWriter := resilientMultiWriter{zerolog.InfoLevel, writers, nil}
logger := zerolog.New(multiWriter).With().Timestamp().Logger().Level(zerolog.InfoLevel) logger := zerolog.New(multiWriter).With().Timestamp().Logger()
logger.Info().Msg("Test msg") logger.Info().Msg("Test msg")
assert.Equal(t, len(writers), writeCalls) for _, w := range test.writers {
writeCalls = 0 // Expect each writer to be written to regardless of the previous writers returning an error
assert.Equal(t, 1, w.writeCalls)
}
})
}
}
type mockedManagementWriter struct {
WriteCalls int
}
func (c *mockedManagementWriter) Write(p []byte) (int, error) {
return len(p), nil
}
func (c *mockedManagementWriter) WriteLevel(level zerolog.Level, p []byte) (int, error) {
c.WriteCalls++
return len(p), nil
}
// Tests that management writer receives write calls of all levels except Disabled
func TestResilientMultiWriter_Management(t *testing.T) {
for _, level := range []zerolog.Level{
zerolog.DebugLevel,
zerolog.InfoLevel,
zerolog.WarnLevel,
zerolog.ErrorLevel,
zerolog.FatalLevel,
zerolog.PanicLevel,
} {
t.Run(level.String(), func(t *testing.T) {
managementWriter := mockedManagementWriter{}
multiWriter := resilientMultiWriter{level, []io.Writer{&mockedWriter{}}, &managementWriter}
logger := zerolog.New(multiWriter).With().Timestamp().Logger()
logger.Info().Msg("Test msg")
// Always write to management
assert.Equal(t, 1, managementWriter.WriteCalls)
})
} }
} }

View File

@ -41,7 +41,7 @@ func NewLogger() *Logger {
log := zerolog.New(zerolog.ConsoleWriter{ log := zerolog.New(zerolog.ConsoleWriter{
Out: os.Stdout, Out: os.Stdout,
TimeFormat: time.RFC3339, TimeFormat: time.RFC3339,
}).With().Timestamp().Logger().Level(zerolog.DebugLevel) }).With().Timestamp().Logger().Level(zerolog.InfoLevel)
return &Logger{ return &Logger{
Log: &log, Log: &log,
} }