From 6bef5646fb0704cc5ac7841486303d3762aa9f8a Mon Sep 17 00:00:00 2001 From: Cavaughn Browne Date: Thu, 8 Dec 2022 15:23:04 -0600 Subject: [PATCH] save cli log output to file and store a copy in the support bundle upon error --- cmd/eks-a-tool/cmd/root.go | 4 +- cmd/eksctl-anywhere/cmd/root.go | 18 ++- cmd/integration_test/cmd/root.go | 4 +- pkg/diagnostics/collector_types.go | 6 + pkg/diagnostics/collectors.go | 23 ++++ pkg/diagnostics/collectors_test.go | 23 ++++ pkg/diagnostics/diagnostic_bundle.go | 12 +- pkg/diagnostics/diagnostic_bundle_test.go | 6 + pkg/diagnostics/interfaces.go | 2 + .../interfaces/mocks/diagnostics.go | 28 +++++ pkg/logger/logger.go | 22 ++-- pkg/logger/logger_wb_test.go | 3 + pkg/logger/zap.go | 107 ++++++++++++++---- pkg/logger/zap_test.go | 100 ++++++++++++++++ test/e2e/simpleflow_test.go | 6 +- .../tools/eks-anywhere-test-tool/cmd/root.go | 4 +- 16 files changed, 329 insertions(+), 39 deletions(-) create mode 100644 pkg/logger/logger_wb_test.go create mode 100644 pkg/logger/zap_test.go diff --git a/cmd/eks-a-tool/cmd/root.go b/cmd/eks-a-tool/cmd/root.go index 40ed487bf1a4..411915e97163 100644 --- a/cmd/eks-a-tool/cmd/root.go +++ b/cmd/eks-a-tool/cmd/root.go @@ -41,7 +41,9 @@ func rootPersistentPreRun(cmd *cobra.Command, args []string) { } func initLogger() error { - if err := logger.InitZap(viper.GetInt("verbosity")); err != nil { + if err := logger.InitZap(logger.ZapOpts{ + Level: viper.GetInt("verbosity"), + }); err != nil { return fmt.Errorf("failed init zap logger in root command: %v", err) } diff --git a/cmd/eksctl-anywhere/cmd/root.go b/cmd/eksctl-anywhere/cmd/root.go index 581566a2ee45..23845cad2bea 100644 --- a/cmd/eksctl-anywhere/cmd/root.go +++ b/cmd/eksctl-anywhere/cmd/root.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "log" + "os" + "time" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -16,6 +18,16 @@ var rootCmd = &cobra.Command{ Short: "Amazon EKS Anywhere", Long: `Use eksctl anywhere to build your own self-managing cluster on your hardware with the best of Amazon EKS`, PersistentPreRun: rootPersistentPreRun, + PersistentPostRun: func(cmd *cobra.Command, args []string) { + outputFilePath := logger.GetOutputFilePath() + if outputFilePath == "" { + return + } + + if err := os.Remove(outputFilePath); err != nil { + fmt.Printf("Failed to cleanup log file %s: %s", outputFilePath, err) + } + }, } func init() { @@ -32,7 +44,11 @@ func rootPersistentPreRun(cmd *cobra.Command, args []string) { } func initLogger() error { - if err := logger.InitZap(viper.GetInt("verbosity")); err != nil { + outputFilePath := fmt.Sprintf("./eksa-cli-%s.log", time.Now().Format("2006-01-02T15_04_05")) + if err := logger.InitZap(logger.ZapOpts{ + Level: viper.GetInt("verbosity"), + OutputFilePath: outputFilePath, + }); err != nil { return fmt.Errorf("failed init zap logger in root command: %v", err) } diff --git a/cmd/integration_test/cmd/root.go b/cmd/integration_test/cmd/root.go index 0f93d7affd87..452b68fa5199 100644 --- a/cmd/integration_test/cmd/root.go +++ b/cmd/integration_test/cmd/root.go @@ -31,7 +31,9 @@ func rootPersistentPreRun(cmd *cobra.Command, args []string) { } func initLogger() error { - if err := logger.InitZap(viper.GetInt("verbosity")); err != nil { + if err := logger.InitZap(logger.ZapOpts{ + Level: viper.GetInt("verbosity"), + }); err != nil { return fmt.Errorf("failed init zap logger in root command: %v", err) } diff --git a/pkg/diagnostics/collector_types.go b/pkg/diagnostics/collector_types.go index 08aab2c22a78..7b1728dbc2f2 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 2fec5dc03fdf..fd7957a984e0 100644 --- a/pkg/diagnostics/collectors.go +++ b/pkg/diagnostics/collectors.go @@ -2,6 +2,7 @@ package diagnostics import ( "fmt" + "path/filepath" "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/files" "github.com/aws/eks-anywhere/pkg/providers" ) @@ -171,6 +173,27 @@ func (c *collectorFactory) PackagesCollectors() []*Collect { return collectors } +func (c *collectorFactory) FileCollectors(paths []string) []*Collect { + collectors := []*Collect{} + + for _, path := range paths { + r := files.NewReader() + content, err := r.ReadFile(path) + if err != nil { + content = []byte(fmt.Sprintf("Failed to retrieve file %s for collection: %s", path, err)) + } + + collectors = append(collectors, &Collect{ + Data: &data{ + Data: string(content), + Name: filepath.Base(path), + }, + }) + } + + return collectors +} + func (c *collectorFactory) getCollectorsMap() map[v1alpha1.OSFamily][]*Collect { return map[v1alpha1.OSFamily][]*Collect{ v1alpha1.Ubuntu: c.ubuntuHostCollectors(), diff --git a/pkg/diagnostics/collectors_test.go b/pkg/diagnostics/collectors_test.go index 5251640fabf5..cb1399b6d326 100644 --- a/pkg/diagnostics/collectors_test.go +++ b/pkg/diagnostics/collectors_test.go @@ -2,6 +2,7 @@ package diagnostics_test import ( "fmt" + "path/filepath" "testing" . "github.com/onsi/gomega" @@ -13,8 +14,30 @@ import ( "github.com/aws/eks-anywhere/pkg/cluster" "github.com/aws/eks-anywhere/pkg/constants" "github.com/aws/eks-anywhere/pkg/diagnostics" + "github.com/aws/eks-anywhere/pkg/filewriter" ) +func TestFileCollectors(t *testing.T) { + g := NewGomegaWithT(t) + factory := diagnostics.NewDefaultCollectorFactory() + + w, err := filewriter.NewWriter(t.TempDir()) + g.Expect(err).To(BeNil()) + + logOut, err := w.Write("test.log", []byte("test content")) + g.Expect(err).To(BeNil()) + g.Expect(logOut).To(BeAnExistingFile()) + + collectors := factory.FileCollectors([]string{logOut}) + g.Expect(collectors).To(HaveLen(1), "DefaultCollectors() mismatch between number of desired collectors and actual") + g.Expect(collectors[0].Data.Data).To(Equal("test content")) + g.Expect(collectors[0].Data.Name).To(Equal(filepath.Base(logOut))) + + collectors = factory.FileCollectors([]string{"does-not-exist.log"}) + g.Expect(collectors).To(HaveLen(1), "DefaultCollectors() mismatch between number of desired collectors and actual") + g.Expect(collectors[0].Data.Data).To(ContainSubstring("Failed to retrieve file does-not-exist.log for collection")) +} + func TestVsphereDataCenterConfigCollectors(t *testing.T) { g := NewGomegaWithT(t) spec := test.NewClusterSpec(func(s *cluster.Spec) { diff --git a/pkg/diagnostics/diagnostic_bundle.go b/pkg/diagnostics/diagnostic_bundle.go index b8ca0e5868e5..07772c0c1b7e 100644 --- a/pkg/diagnostics/diagnostic_bundle.go +++ b/pkg/diagnostics/diagnostic_bundle.go @@ -70,6 +70,7 @@ func newDiagnosticBundleManagementCluster(af AnalyzerFactory, cf CollectorFactor } b.WithDefaultCollectors(). + WithFileCollectors([]string{logger.GetOutputFilePath()}). WithDefaultAnalyzers(). WithManagementCluster(true). WithDatacenterConfig(spec.Cluster.Spec.DatacenterRef, spec). @@ -116,6 +117,7 @@ func newDiagnosticBundleFromSpec(af AnalyzerFactory, cf CollectorFactory, spec * WithManagementCluster(spec.Cluster.IsSelfManaged()). WithDefaultAnalyzers(). WithDefaultCollectors(). + WithFileCollectors([]string{logger.GetOutputFilePath()}). WithPackagesCollectors(). WithLogTextAnalyzers() @@ -142,7 +144,9 @@ func newDiagnosticBundleDefault(af AnalyzerFactory, cf CollectorFactory) *EksaDi analyzerFactory: af, collectorFactory: cf, } - return b.WithDefaultAnalyzers().WithDefaultCollectors().WithManagementCluster(true) + return b.WithDefaultAnalyzers(). + WithDefaultCollectors(). + WithManagementCluster(true) } func newDiagnosticBundleCustom(af AnalyzerFactory, cf CollectorFactory, client BundleClient, kubectl *executables.Kubectl, bundlePath string, kubeconfig string, writer filewriter.FileWriter) *EksaDiagnosticBundle { @@ -260,6 +264,12 @@ func (e *EksaDiagnosticBundle) WithManagementCluster(isSelfManaged bool) *EksaDi return e } +// WithFileCollectors appends collectors that collect static data from the specified paths to the bundle. +func (e *EksaDiagnosticBundle) WithFileCollectors(paths []string) *EksaDiagnosticBundle { + e.bundle.Spec.Collectors = append(e.bundle.Spec.Collectors, e.collectorFactory.FileCollectors(paths)...) + return e +} + func (e *EksaDiagnosticBundle) WithPackagesCollectors() *EksaDiagnosticBundle { e.bundle.Spec.Analyzers = append(e.bundle.Spec.Analyzers, e.analyzerFactory.PackageAnalyzers()...) e.bundle.Spec.Collectors = append(e.bundle.Spec.Collectors, e.collectorFactory.PackagesCollectors()...) diff --git a/pkg/diagnostics/diagnostic_bundle_test.go b/pkg/diagnostics/diagnostic_bundle_test.go index 02da491dd28b..cb06e2b6ac73 100644 --- a/pkg/diagnostics/diagnostic_bundle_test.go +++ b/pkg/diagnostics/diagnostic_bundle_test.go @@ -129,6 +129,7 @@ func TestGenerateBundleConfigWithExternalEtcd(t *testing.T) { c.EXPECT().ManagementClusterCollectors().Return(nil) c.EXPECT().DataCenterConfigCollectors(spec.Cluster.Spec.DatacenterRef, spec).Return(nil) c.EXPECT().PackagesCollectors().Return(nil) + c.EXPECT().FileCollectors(gomock.Any()).Return(nil) w := givenWriter(t) w.EXPECT().Write(gomock.Any(), gomock.Any()) @@ -191,6 +192,7 @@ func TestGenerateBundleConfigWithOidc(t *testing.T) { c.EXPECT().ManagementClusterCollectors().Return(nil) c.EXPECT().DataCenterConfigCollectors(spec.Cluster.Spec.DatacenterRef, spec).Return(nil) c.EXPECT().PackagesCollectors().Return(nil) + c.EXPECT().FileCollectors(gomock.Any()).Return(nil) opts := diagnostics.EksaDiagnosticBundleFactoryOpts{ AnalyzerFactory: a, @@ -250,6 +252,7 @@ func TestGenerateBundleConfigWithGitOps(t *testing.T) { c.EXPECT().ManagementClusterCollectors().Return(nil) c.EXPECT().DataCenterConfigCollectors(spec.Cluster.Spec.DatacenterRef, spec).Return(nil) c.EXPECT().PackagesCollectors().Return(nil) + c.EXPECT().FileCollectors(gomock.Any()).Return(nil) opts := diagnostics.EksaDiagnosticBundleFactoryOpts{ AnalyzerFactory: a, @@ -333,6 +336,7 @@ func TestBundleFromSpecComplete(t *testing.T) { c.EXPECT().ManagementClusterCollectors().Return(nil) c.EXPECT().DataCenterConfigCollectors(spec.Cluster.Spec.DatacenterRef, spec).Return(nil) c.EXPECT().PackagesCollectors().Return(nil) + c.EXPECT().FileCollectors(gomock.Any()).Return(nil) w := givenWriter(t) w.EXPECT().Write(gomock.Any(), gomock.Any()).Times(2) @@ -475,6 +479,7 @@ func TestGenerateManagementClusterBundleVsphereProvider(t *testing.T) { c.EXPECT().DefaultCollectors().Return(nil) c.EXPECT().ManagementClusterCollectors().Return(nil) c.EXPECT().DataCenterConfigCollectors(spec.Cluster.Spec.DatacenterRef, spec).Return(nil) + c.EXPECT().FileCollectors(gomock.Any()).Return(nil) w := givenWriter(t) w.EXPECT().Write(gomock.Any(), gomock.Any()).Times(2) @@ -522,6 +527,7 @@ func TestGenerateManagementClusterBundleDockerProvider(t *testing.T) { c.EXPECT().DefaultCollectors().Return(nil) c.EXPECT().ManagementClusterCollectors().Return(nil) c.EXPECT().DataCenterConfigCollectors(spec.Cluster.Spec.DatacenterRef, spec).Return(nil) + c.EXPECT().FileCollectors(gomock.Any()).Return(nil) w := givenWriter(t) w.EXPECT().Write(gomock.Any(), gomock.Any()).Times(2) diff --git a/pkg/diagnostics/interfaces.go b/pkg/diagnostics/interfaces.go index 541342c4928d..e6b937158dac 100644 --- a/pkg/diagnostics/interfaces.go +++ b/pkg/diagnostics/interfaces.go @@ -31,6 +31,7 @@ type DiagnosticBundle interface { CollectAndAnalyze(ctx context.Context, sinceTimeValue *time.Time) error WithDefaultAnalyzers() *EksaDiagnosticBundle WithDefaultCollectors() *EksaDiagnosticBundle + WithFileCollectors(paths []string) *EksaDiagnosticBundle WithDatacenterConfig(config v1alpha1.Ref, spec *cluster.Spec) *EksaDiagnosticBundle WithOidcConfig(config *v1alpha1.OIDCConfig) *EksaDiagnosticBundle WithExternalEtcd(config *v1alpha1.ExternalEtcdConfiguration) *EksaDiagnosticBundle @@ -53,6 +54,7 @@ type AnalyzerFactory interface { type CollectorFactory interface { PackagesCollectors() []*Collect DefaultCollectors() []*Collect + FileCollectors(paths []string) []*Collect ManagementClusterCollectors() []*Collect EksaHostCollectors(configs []providers.MachineConfig) []*Collect DataCenterConfigCollectors(datacenter v1alpha1.Ref, spec *cluster.Spec) []*Collect diff --git a/pkg/diagnostics/interfaces/mocks/diagnostics.go b/pkg/diagnostics/interfaces/mocks/diagnostics.go index c32f00b3e54a..a82166362e38 100644 --- a/pkg/diagnostics/interfaces/mocks/diagnostics.go +++ b/pkg/diagnostics/interfaces/mocks/diagnostics.go @@ -287,6 +287,20 @@ func (mr *MockDiagnosticBundleMockRecorder) WithExternalEtcd(config interface{}) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithExternalEtcd", reflect.TypeOf((*MockDiagnosticBundle)(nil).WithExternalEtcd), config) } +// WithFileCollectors mocks base method. +func (m *MockDiagnosticBundle) WithFileCollectors(paths []string) *diagnostics.EksaDiagnosticBundle { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WithFileCollectors", paths) + ret0, _ := ret[0].(*diagnostics.EksaDiagnosticBundle) + return ret0 +} + +// WithFileCollectors indicates an expected call of WithFileCollectors. +func (mr *MockDiagnosticBundleMockRecorder) WithFileCollectors(paths interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithFileCollectors", reflect.TypeOf((*MockDiagnosticBundle)(nil).WithFileCollectors), paths) +} + // WithGitOpsConfig mocks base method. func (m *MockDiagnosticBundle) WithGitOpsConfig(config *v1alpha1.GitOpsConfig) *diagnostics.EksaDiagnosticBundle { m.ctrl.T.Helper() @@ -572,6 +586,20 @@ func (mr *MockCollectorFactoryMockRecorder) EksaHostCollectors(configs interface return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EksaHostCollectors", reflect.TypeOf((*MockCollectorFactory)(nil).EksaHostCollectors), configs) } +// FileCollectors mocks base method. +func (m *MockCollectorFactory) FileCollectors(paths []string) []*diagnostics.Collect { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FileCollectors", paths) + ret0, _ := ret[0].([]*diagnostics.Collect) + return ret0 +} + +// FileCollectors indicates an expected call of FileCollectors. +func (mr *MockCollectorFactoryMockRecorder) FileCollectors(paths interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FileCollectors", reflect.TypeOf((*MockCollectorFactory)(nil).FileCollectors), paths) +} + // ManagementClusterCollectors mocks base method. func (m *MockCollectorFactory) ManagementClusterCollectors() []*diagnostics.Collect { m.ctrl.T.Helper() diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 88a11795e511..88fe26d26453 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -16,16 +16,24 @@ const ( ) var ( - l logr.Logger = logr.Discard() - once sync.Once + l logr.Logger = logr.Discard() + once sync.Once + outputFilePath string ) -func set(logger logr.Logger) { +func set(logger logr.Logger, out string) { once.Do(func() { l = logger + outputFilePath = out }) } +// GetOutputFilePath returns the path to the file where high verbosity logs are written to. +// If the logger hasn't been configured to output to a file, it returns an empty string. +func GetOutputFilePath() string { + return outputFilePath +} + // 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 { @@ -83,11 +91,3 @@ func MarkFail(msg string, keysAndValues ...interface{}) { func MarkWarning(msg string, keysAndValues ...interface{}) { l.V(0).Info(markWarning+msg, keysAndValues...) } - -type LoggerOpt func(logr *logr.Logger) - -func WithName(name string) LoggerOpt { - return func(logr *logr.Logger) { - *logr = (*logr).WithName(name) - } -} diff --git a/pkg/logger/logger_wb_test.go b/pkg/logger/logger_wb_test.go new file mode 100644 index 000000000000..ce58a6c425b1 --- /dev/null +++ b/pkg/logger/logger_wb_test.go @@ -0,0 +1,3 @@ +package logger + +var NewZap = newZap diff --git a/pkg/logger/zap.go b/pkg/logger/zap.go index 3b53be9f735c..4e839b87c29b 100644 --- a/pkg/logger/zap.go +++ b/pkg/logger/zap.go @@ -2,53 +2,116 @@ package logger import ( "fmt" + "os" "time" + "github.com/go-logr/logr" "github.com/go-logr/zapr" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) +// ZapOpts represents a set of arguments for initializing the zap logger. +type ZapOpts struct { + Level int // indicates the log level of the logger. + OutputFilePath string // if specified, the logger will output to file at this path. + WithNames []string // specified name elements are added to the logger's name. +} + // InitZap creates a zap logger with the provided verbosity level // and sets it as the package logger. // 0 is the least verbose and 10 the most verbose. // The package logger can only be init once, so subsequent calls to this method // won't have any effect. -func InitZap(level int, opts ...LoggerOpt) error { - cfg := zap.NewDevelopmentConfig() - cfg.Level = zap.NewAtomicLevelAt(zapcore.Level(-1 * level)) - cfg.EncoderConfig.EncodeLevel = nil - cfg.EncoderConfig.EncodeTime = NullTimeEncoder - cfg.DisableCaller = true - cfg.DisableStacktrace = true +func InitZap(args ZapOpts) error { + logr, err := newZap(args) + if err != nil { + return err + } + set(logr, args.OutputFilePath) + l.V(4).Info("Logger init completed", "vlevel", args.Level) + + return nil +} + +// VLevelEncoder serializes a Level to V + v-level number,. +func VLevelEncoder(l zapcore.Level, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendString(fmt.Sprintf("V%d", -1*int(l))) +} + +// NullTimeEncoder skips time serialization. +func NullTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {} + +func newZap(args ZapOpts) (logr.Logger, error) { + outputPaths := []string{} + if args.OutputFilePath != "" { + outputPaths = append(outputPaths, args.OutputFilePath) + } + + cfg := config{ + level: args.Level, + encoderConfig: zap.NewDevelopmentEncoderConfig(), + outputPaths: outputPaths, + } + + cfg.encoderConfig.EncodeLevel = nil + cfg.encoderConfig.EncodeTime = NullTimeEncoder // Only enabling this at level 4 because that's when // our debugging levels start. Ref: doc.go - if level >= 4 { - cfg.EncoderConfig.EncodeLevel = VLevelEncoder - cfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder + if args.Level >= 4 { + cfg.encoderConfig.EncodeLevel = VLevelEncoder + cfg.encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder } - zapLog, err := cfg.Build() + zapLog, err := build(cfg) if err != nil { - return fmt.Errorf("creating zap logger: %v", err) + return logr.Discard(), fmt.Errorf("creating zap logger: %v", err) } logr := zapr.NewLogger(zapLog) - for _, opt := range opts { - opt(&logr) + + for _, name := range args.WithNames { + logr = logr.WithName(name) } - set(logr) - l.V(4).Info("Logger init completed", "vlevel", level) + return logr, err +} - return nil +// newAtomicLevelAt returns an appropriate zap.AtomicLevel given an integer representing the log level. +func newAtomicLevelAt(level int) zap.AtomicLevel { + return zap.NewAtomicLevelAt(zapcore.Level(-1 * level)) } -// VLevelEncoder serializes a Level to V + v-level number,. -func VLevelEncoder(l zapcore.Level, enc zapcore.PrimitiveArrayEncoder) { - enc.AppendString(fmt.Sprintf("V%d", -1*int(l))) +// build constructs a logger and returns a logger. +func build(cfg config) (*zap.Logger, error) { + sink, err := cfg.openSinks() + if err != nil { + return nil, err + } + + logger := zap.New(cfg.buildCore(sink)) + return logger, nil } -// NullTimeEncoder skips time serialization. -func NullTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {} +func (cfg config) openSinks() (zapcore.WriteSyncer, error) { + sink, _, err := zap.Open(cfg.outputPaths...) + return sink, err +} + +// config helps to construct a customized zap logger. +type config struct { + outputPaths []string + level int + encoderConfig zapcore.EncoderConfig +} + +func (cfg config) buildCore(sink zapcore.WriteSyncer) zapcore.Core { + fileEncoder := zapcore.NewJSONEncoder(cfg.encoderConfig) + consoleEncoder := zapcore.NewConsoleEncoder(cfg.encoderConfig) + + return zapcore.NewTee( + zapcore.NewCore(consoleEncoder, zapcore.AddSync(os.Stdout), newAtomicLevelAt(cfg.level)), + zapcore.NewCore(fileEncoder, zapcore.AddSync(sink), newAtomicLevelAt(9)), + ) +} diff --git a/pkg/logger/zap_test.go b/pkg/logger/zap_test.go new file mode 100644 index 000000000000..e79f6c677e03 --- /dev/null +++ b/pkg/logger/zap_test.go @@ -0,0 +1,100 @@ +package logger_test + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "testing" + "time" + + "github.com/go-logr/logr" + . "github.com/onsi/gomega" + + "github.com/aws/eks-anywhere/pkg/logger" +) + +func TestInitZap(t *testing.T) { + g := NewWithT(t) + logFile := filepath.Join(t.TempDir(), "test.log") + err := logger.InitZap(logger.ZapOpts{ + Level: 0, + OutputFilePath: logFile, + }) + g.Expect(err).To(BeNil()) + g.Expect(logger.Get()).ToNot(Equal(logr.Discard())) + g.Expect(logger.GetOutputFilePath()).To(Equal(logFile)) +} + +func TestNewZapWithNames(t *testing.T) { + g := NewWithT(t) + logOut := filepath.Join(t.TempDir(), "test.log") + l, err := logger.NewZap(logger.ZapOpts{ + Level: 0, + OutputFilePath: logOut, + WithNames: []string{"test-logger"}, + }) + + g.Expect(err).To(BeNil()) + g.Expect(l).ToNot(Equal(logr.Discard())) + + l.Info("debug log with name") + l.Error(errors.New("test error"), "error log with name") + + byteContents, err := os.ReadFile(logOut) + g.Expect(err).To(BeNil()) + g.Expect(string(byteContents)).To(ContainSubstring("\"N\":\"test-logger\",\"M\":\"debug log with name\"")) + g.Expect(string(byteContents)).To(ContainSubstring("\"N\":\"test-logger\",\"M\":\"error log with name\",\"error\":\"test error\"")) +} + +func TestNewZapWithOutputFilePath(t *testing.T) { + g := NewWithT(t) + logOut := filepath.Join(t.TempDir(), "test.log") + l, err := logger.NewZap(logger.ZapOpts{ + Level: 0, + OutputFilePath: logOut, + }) + + g.Expect(err).To(BeNil()) + g.Expect(l).ToNot(Equal(logr.Discard())) + + l.Info("debug log") + l.Error(errors.New("test error"), "error log") + + byteContents, err := os.ReadFile(logOut) + g.Expect(err).To(BeNil()) + g.Expect(string(byteContents)).To(ContainSubstring("\"M\":\"debug log\"")) + g.Expect(string(byteContents)).To(ContainSubstring("\"M\":\"error log\",\"error\":\"test error\"")) +} + +func TestZapWithInvalidOutputPaths(t *testing.T) { + tests := []struct { + name string + path string + match string + }{ + { + name: "output path doesn't exist", + path: "/temp/does-not-exist/foo.log", + match: "no such file or directory", + }, + { + name: "bad schema", + path: fmt.Sprintf("foo-%s.log", time.Now().Format("2006-01-02T15:04:05")), + match: "no sink found for scheme", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + l, err := logger.NewZap(logger.ZapOpts{ + Level: 0, + OutputFilePath: tt.path, + }) + + g.Expect(l).To(Equal(logr.Discard())) + g.Expect(err).To(MatchError(ContainSubstring(tt.match))) + }) + } +} diff --git a/test/e2e/simpleflow_test.go b/test/e2e/simpleflow_test.go index 030ac34e17e7..836aaa53f0b6 100644 --- a/test/e2e/simpleflow_test.go +++ b/test/e2e/simpleflow_test.go @@ -16,7 +16,11 @@ import ( ) func init() { - if err := logger.InitZap(4, logger.WithName("e2e")); err != nil { + args := logger.ZapOpts{ + Level: 4, + WithNames: []string{"e2e"}, + } + if err := logger.InitZap(args); err != nil { log.Fatal(fmt.Errorf("failed init zap logger for e2e tests: %v", err)) } } diff --git a/test/e2e/tools/eks-anywhere-test-tool/cmd/root.go b/test/e2e/tools/eks-anywhere-test-tool/cmd/root.go index 1b345841d3fe..89d350a287f0 100644 --- a/test/e2e/tools/eks-anywhere-test-tool/cmd/root.go +++ b/test/e2e/tools/eks-anywhere-test-tool/cmd/root.go @@ -31,7 +31,9 @@ func rootPersistentPreRun(cmd *cobra.Command, args []string) { } func initLogger() error { - if err := logger.InitZap(viper.GetInt("verbosity")); err != nil { + if err := logger.InitZap(logger.ZapOpts{ + Level: viper.GetInt("verbosity"), + }); err != nil { return fmt.Errorf("failed init zap logger in root command: %v", err) }