From 9dfc2b8004db38a93f344e891052c58885ce3095 Mon Sep 17 00:00:00 2001 From: Cavaughn Browne Date: Mon, 5 Dec 2022 17:55:49 -0600 Subject: [PATCH] saving cli log output to support bundle --- pkg/diagnostics/collector_types.go | 6 ++ pkg/diagnostics/collectors.go | 15 ++- pkg/diagnostics/diagnostic_bundle.go | 2 + pkg/logger/caching_logsink.go | 133 +++++++++++++++++++++++++++ pkg/logger/logger.go | 13 ++- 5 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 pkg/logger/caching_logsink.go diff --git a/pkg/diagnostics/collector_types.go b/pkg/diagnostics/collector_types.go index 08aab2c22a78b..7b1728dbc2f28 100644 --- a/pkg/diagnostics/collector_types.go +++ b/pkg/diagnostics/collector_types.go @@ -10,6 +10,7 @@ type Collect struct { ClusterResources *clusterResources `json:"clusterResources,omitempty"` Secret *secret `json:"secret,omitempty"` Logs *logs `json:"logs,omitempty"` + Data *data `json:"data,omitempty"` CopyFromHost *copyFromHost `json:"copyFromHost,omitempty"` Exec *exec `json:"exec,omitempty"` RunPod *runPod `json:"runPod,omitempty"` @@ -46,6 +47,11 @@ type logs struct { Limits *logLimits `json:"limits,omitempty"` } +type data struct { + Name string `json:"name,omitempty"` + Data string `json:"data,omitempty"` +} + type copyFromHost struct { collectorMeta `json:",inline"` Name string `json:"name,omitempty"` diff --git a/pkg/diagnostics/collectors.go b/pkg/diagnostics/collectors.go index 2fec5dc03fdf8..a8696d8f1d9a8 100644 --- a/pkg/diagnostics/collectors.go +++ b/pkg/diagnostics/collectors.go @@ -2,6 +2,7 @@ package diagnostics import ( "fmt" + "strings" "time" v1 "k8s.io/api/core/v1" @@ -9,6 +10,7 @@ import ( "github.com/aws/eks-anywhere/pkg/api/v1alpha1" "github.com/aws/eks-anywhere/pkg/cluster" "github.com/aws/eks-anywhere/pkg/constants" + "github.com/aws/eks-anywhere/pkg/logger" "github.com/aws/eks-anywhere/pkg/providers" ) @@ -43,7 +45,7 @@ func (c *collectorFactory) DefaultCollectors() []*Collect { }, }, } - collectors = append(collectors, c.defaultLogCollectors()...) + collectors = append(collectors, append(c.defaultLogCollectors(), c.defaultStaticDataCollectors()...)...) return collectors } @@ -212,6 +214,17 @@ func (c *collectorFactory) ubuntuHostCollectors() []*Collect { } } +func (c *collectorFactory) defaultStaticDataCollectors() []*Collect { + return []*Collect{ + { + Data: &data{ + Data: strings.Join(logger.GetLogs(9), "\n"), + Name: logpath("cli.log"), + }, + }, + } +} + func (c *collectorFactory) defaultLogCollectors() []*Collect { return []*Collect{ { diff --git a/pkg/diagnostics/diagnostic_bundle.go b/pkg/diagnostics/diagnostic_bundle.go index b8ca0e5868e5d..0f0572caf21af 100644 --- a/pkg/diagnostics/diagnostic_bundle.go +++ b/pkg/diagnostics/diagnostic_bundle.go @@ -206,6 +206,7 @@ func (e *EksaDiagnosticBundle) WriteBundleConfig() error { if err != nil { return err } + logger.V(3).Info("bundle config written", "path", e.bundlePath) return nil } @@ -243,6 +244,7 @@ func (e *EksaDiagnosticBundle) WriteAnalysisToFile() (path string, err error) { } func (e *EksaDiagnosticBundle) WithDefaultCollectors() *EksaDiagnosticBundle { + // Add static data collector as a default collector for cli logs e.bundle.Spec.Collectors = append(e.bundle.Spec.Collectors, e.collectorFactory.DefaultCollectors()...) return e } diff --git a/pkg/logger/caching_logsink.go b/pkg/logger/caching_logsink.go new file mode 100644 index 0000000000000..024bedf286792 --- /dev/null +++ b/pkg/logger/caching_logsink.go @@ -0,0 +1,133 @@ +package logger + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/go-logr/logr" +) + +type log struct { + timestamp string + level int + msg string + keyAndValues []interface{} +} + +// CachingLogSink enhances the provided logger's implementation to save logs to memory. +type CachingLogSink struct { + logger logr.Logger + logs []log +} + +func (m *CachingLogSink) getLogs(level int) []log { + filtered := []log{} + + for _, log := range m.logs { + if log.level <= level { + filtered = append(filtered, log) + } + } + + return filtered +} + +func (l *log) keyAndValuesObject() map[string]string { + m := make(map[string]string) + for i := 0; i < len(l.keyAndValues); i += 2 { + strKey := fmt.Sprintf("%v", l.keyAndValues[i]) + value := l.keyAndValues[i+1] + if value != nil || value == "" { + m[strKey] = "null" + } else { + m[strKey] = fmt.Sprintf("%v", value) + } + + } + + return m +} + +func (l *log) keyAndValuesObjectStr() string { + m := l.keyAndValuesObject() + mJSON, err := json.Marshal(m) + if err != nil { + return fmt.Sprintf("%v", m) + } + + return string(mJSON) +} + +func formatLog(l log) string { + if l.level < 0 { + return fmt.Sprintf("%s \t %s", l.msg, l.keyAndValuesObjectStr()) + } + + return fmt.Sprintf("%s \t %s \t %s \t %s", l.timestamp, fmt.Sprintf("V%d", l.level), l.msg, l.keyAndValuesObjectStr()) +} + +// SetLogger assigns the provided Logger as a base for logging. +func (m *CachingLogSink) SetLogger(l logr.Logger) { + m.logger = l +} + +func (m *CachingLogSink) save(level int, msg string, keysAndValues ...interface{}) { + timestamp := time.Now().Format("2006-01-02T15:04:05.000-0600") + m.logs = append(m.logs, log{timestamp, level, msg, keysAndValues}) +} + +// Init receives optional information about the logr library for LogSink +// implementations that need it. +func (m *CachingLogSink) Init(info logr.RuntimeInfo) { + m.logger.GetSink().Init(info) +} + +// Enabled tests whether this LogSink is enabled at the specified V-level. +// For example, commandline flags might be used to set the logging +// verbosity and disable some info logs. +func (m *CachingLogSink) Enabled(level int) bool { + return m.logger.GetSink().Enabled(level) +} + +// Info logs a non-error message with the given key/value pairs as context. +// +// The msg argument should be used to add some constant description to +// the log line. The key/value pairs can then be used to add additional +// variable information. The key/value pairs should alternate string +// keys and arbitrary values. +func (m *CachingLogSink) Info(level int, msg string, keysAndValues ...interface{}) { + m.logger.GetSink().Info(level, msg, keysAndValues...) + m.save(level, msg, keysAndValues...) +} + +// Error logs an error, with the given message and key/value pairs as +// context. See Logger.Error for more details. +func (m *CachingLogSink) Error(err error, msg string, keysAndValues ...interface{}) { + m.logger.GetSink().Error(err, msg, keysAndValues...) + m.save(-1, msg, append(keysAndValues, err)...) +} + +// WithValues returns a new LogSink with additional key/value pairs. See +// Logger.WithValues for more details. +func (m *CachingLogSink) WithValues(keysAndValues ...interface{}) logr.LogSink { + return m.logger.GetSink().WithValues(keysAndValues...) +} + +// WithName returns a new LogSink with the specified name appended. See +// Logger.WithName for more details. +func (m *CachingLogSink) WithName(name string) logr.LogSink { + return m.logger.GetSink().WithName(name) +} + +// GetLogs returns the logs stored in memory logged from the registered Logger. +func (m *CachingLogSink) GetLogs(level int) []string { + formatted := []string{} + for _, log := range m.getLogs(level) { + if log.level <= level { + formatted = append(formatted, formatLog(log)) + } + } + + return formatted +} diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 88a11795e5113..393579a371091 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -16,16 +16,23 @@ const ( ) var ( - l logr.Logger = logr.Discard() - once sync.Once + l logr.Logger = logr.Discard() + cachingLogSink = CachingLogSink{logger: l} + once sync.Once ) func set(logger logr.Logger) { once.Do(func() { - l = logger + cachingLogSink.SetLogger(logger) + l = logr.New(&cachingLogSink) }) } +// Get Logs returns the stored in memory logs based on the provided verbosity level. +func GetLogs(level int) []string { + return cachingLogSink.GetLogs(level) +} + // Get returns the logger instance that has been previously set. // If no logger has been set, it returns a null logger. func Get() logr.Logger {