diff --git a/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/logger/custom_receiver.go b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/logger/custom_receiver.go new file mode 100644 index 00000000000..47fcae32dad --- /dev/null +++ b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/logger/custom_receiver.go @@ -0,0 +1,61 @@ +package logger + +import ( + "fmt" + + "github.com/cihub/seelog" +) + +// CustomReceiver defines the interface for custom logging implementations +type CustomReceiver interface { + GetOutputFormat() string + Trace(message string) + Debug(message string) + Info(message string) + Warn(message string) + Error(message string) + Critical(message string) + Flush() error + Close() error +} + +// internal wrapper that implements seelog.CustomReceiver +type customReceiverWrapper struct { + receiver CustomReceiver +} + +func (w *customReceiverWrapper) ReceiveMessage(message string, level seelog.LogLevel, context seelog.LogContextInterface) error { + + switch level { + case seelog.TraceLvl: + w.receiver.Trace(message) + case seelog.DebugLvl: + w.receiver.Debug(message) + case seelog.InfoLvl: + w.receiver.Info(message) + case seelog.WarnLvl: + w.receiver.Warn(message) + case seelog.ErrorLvl: + w.receiver.Error(message) + case seelog.CriticalLvl: + w.receiver.Critical(message) + default: + fmt.Printf("Unhandled level: %v", level) + } + return nil +} + +func (w *customReceiverWrapper) AfterParse(initArgs seelog.CustomReceiverInitArgs) error { + return nil +} + +func (w *customReceiverWrapper) Flush() { + err := w.receiver.Flush() + if err != nil { + fmt.Printf("Couldn't flush the logger due to: %v", err) + } +} + +func (w *customReceiverWrapper) Close() error { + return w.receiver.Close() +} diff --git a/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/logger/log.go b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/logger/log.go index 5044a3e74e7..3540f71beda 100644 --- a/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/logger/log.go +++ b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/logger/log.go @@ -339,6 +339,49 @@ func SetLogToStdout(duplicate bool) { reloadConfig() } +// SetCustomReceiver configures the ECS Agent logger to use a custom logger implementation. +// This allows external applications to intercept and handle ECS Agent logs in their own way, +// such as sending logs to a custom destination or formatting them differently. +// More details can be found here: https://github.com/cihub/seelog/wiki/Custom-receivers +// The custom receiver must implement the CustomReceiver interface, which requires: +// - GetTimestampFormat(): returns the desired timestamp format (e.g., "2006-01-02T15:04:05Z07:00") +// - GetOutputFormat(): returns the desired output format ("logfmt", "json", or "windows") +// - Log handling methods (Debug, Info, Warn, etc.) +func SetCustomReceiver(receiver CustomReceiver) { + registerCustomFormatters() + wrapper := &customReceiverWrapper{receiver: receiver} + outputFormat := receiver.GetOutputFormat() + + // Internal seelog configuration + customConfig := ` + + + + + + + + + + + ` + + parserParams := &seelog.CfgParseParams{ + CustomReceiverProducers: map[string]seelog.CustomReceiverProducer{ + "customReceiver": func(seelog.CustomReceiverInitArgs) (seelog.CustomReceiver, error) { + return wrapper, nil + }, + }, + } + + replacementLogger, err := seelog.LoggerFromParamConfigAsString(customConfig, parserParams) + if err != nil { + fmt.Println("Failed to create a replacement logger", err) + } + + setGlobalLogger(replacementLogger, outputFormat) +} + func init() { Config = &logConfig{ logfile: os.Getenv(LOGFILE_ENV_VAR), @@ -352,10 +395,7 @@ func init() { } } -// InitSeelog registers custom logging formats, updates the internal Config struct -// and reloads the global logger. This should only be called once, as external -// callers should use the Config struct over environment variables directly. -func InitSeelog() { +func registerCustomFormatters() { if err := seelog.RegisterCustomFormatter("EcsAgentLogfmt", logfmtFormatter); err != nil { seelog.Error(err) } @@ -365,6 +405,13 @@ func InitSeelog() { if err := seelog.RegisterCustomFormatter("EcsMsg", ecsMsgFormatter); err != nil { seelog.Error(err) } +} + +// InitSeelog registers custom logging formats, updates the internal Config struct +// and reloads the global logger. This should only be called once, as external +// callers should use the Config struct over environment variables directly. +func InitSeelog() { + registerCustomFormatters() if DriverLogLevel := os.Getenv(LOGLEVEL_ENV_VAR); DriverLogLevel != "" { SetDriverLogLevel(DriverLogLevel) diff --git a/ecs-agent/logger/custom_receiver.go b/ecs-agent/logger/custom_receiver.go new file mode 100644 index 00000000000..47fcae32dad --- /dev/null +++ b/ecs-agent/logger/custom_receiver.go @@ -0,0 +1,61 @@ +package logger + +import ( + "fmt" + + "github.com/cihub/seelog" +) + +// CustomReceiver defines the interface for custom logging implementations +type CustomReceiver interface { + GetOutputFormat() string + Trace(message string) + Debug(message string) + Info(message string) + Warn(message string) + Error(message string) + Critical(message string) + Flush() error + Close() error +} + +// internal wrapper that implements seelog.CustomReceiver +type customReceiverWrapper struct { + receiver CustomReceiver +} + +func (w *customReceiverWrapper) ReceiveMessage(message string, level seelog.LogLevel, context seelog.LogContextInterface) error { + + switch level { + case seelog.TraceLvl: + w.receiver.Trace(message) + case seelog.DebugLvl: + w.receiver.Debug(message) + case seelog.InfoLvl: + w.receiver.Info(message) + case seelog.WarnLvl: + w.receiver.Warn(message) + case seelog.ErrorLvl: + w.receiver.Error(message) + case seelog.CriticalLvl: + w.receiver.Critical(message) + default: + fmt.Printf("Unhandled level: %v", level) + } + return nil +} + +func (w *customReceiverWrapper) AfterParse(initArgs seelog.CustomReceiverInitArgs) error { + return nil +} + +func (w *customReceiverWrapper) Flush() { + err := w.receiver.Flush() + if err != nil { + fmt.Printf("Couldn't flush the logger due to: %v", err) + } +} + +func (w *customReceiverWrapper) Close() error { + return w.receiver.Close() +} diff --git a/ecs-agent/logger/log.go b/ecs-agent/logger/log.go index 5044a3e74e7..3540f71beda 100644 --- a/ecs-agent/logger/log.go +++ b/ecs-agent/logger/log.go @@ -339,6 +339,49 @@ func SetLogToStdout(duplicate bool) { reloadConfig() } +// SetCustomReceiver configures the ECS Agent logger to use a custom logger implementation. +// This allows external applications to intercept and handle ECS Agent logs in their own way, +// such as sending logs to a custom destination or formatting them differently. +// More details can be found here: https://github.com/cihub/seelog/wiki/Custom-receivers +// The custom receiver must implement the CustomReceiver interface, which requires: +// - GetTimestampFormat(): returns the desired timestamp format (e.g., "2006-01-02T15:04:05Z07:00") +// - GetOutputFormat(): returns the desired output format ("logfmt", "json", or "windows") +// - Log handling methods (Debug, Info, Warn, etc.) +func SetCustomReceiver(receiver CustomReceiver) { + registerCustomFormatters() + wrapper := &customReceiverWrapper{receiver: receiver} + outputFormat := receiver.GetOutputFormat() + + // Internal seelog configuration + customConfig := ` + + + + + + + + + + + ` + + parserParams := &seelog.CfgParseParams{ + CustomReceiverProducers: map[string]seelog.CustomReceiverProducer{ + "customReceiver": func(seelog.CustomReceiverInitArgs) (seelog.CustomReceiver, error) { + return wrapper, nil + }, + }, + } + + replacementLogger, err := seelog.LoggerFromParamConfigAsString(customConfig, parserParams) + if err != nil { + fmt.Println("Failed to create a replacement logger", err) + } + + setGlobalLogger(replacementLogger, outputFormat) +} + func init() { Config = &logConfig{ logfile: os.Getenv(LOGFILE_ENV_VAR), @@ -352,10 +395,7 @@ func init() { } } -// InitSeelog registers custom logging formats, updates the internal Config struct -// and reloads the global logger. This should only be called once, as external -// callers should use the Config struct over environment variables directly. -func InitSeelog() { +func registerCustomFormatters() { if err := seelog.RegisterCustomFormatter("EcsAgentLogfmt", logfmtFormatter); err != nil { seelog.Error(err) } @@ -365,6 +405,13 @@ func InitSeelog() { if err := seelog.RegisterCustomFormatter("EcsMsg", ecsMsgFormatter); err != nil { seelog.Error(err) } +} + +// InitSeelog registers custom logging formats, updates the internal Config struct +// and reloads the global logger. This should only be called once, as external +// callers should use the Config struct over environment variables directly. +func InitSeelog() { + registerCustomFormatters() if DriverLogLevel := os.Getenv(LOGLEVEL_ENV_VAR); DriverLogLevel != "" { SetDriverLogLevel(DriverLogLevel) diff --git a/ecs-agent/logger/log_test.go b/ecs-agent/logger/log_test.go index e8b098d231d..d891d96838f 100644 --- a/ecs-agent/logger/log_test.go +++ b/ecs-agent/logger/log_test.go @@ -17,10 +17,13 @@ package logger import ( + "fmt" "os" + "sync" "testing" "time" + mock_seelog "github.com/aws/amazon-ecs-agent/ecs-agent/logger/mocks" "github.com/cihub/seelog" "github.com/stretchr/testify/require" ) @@ -294,3 +297,118 @@ func (l *LogContextMock) CallTime() time.Time { func (l *LogContextMock) CustomContext() interface{} { return map[string]string{} } + +func TestSetCustomLogger(t *testing.T) { + // Store the original logger to restore it later + originalLogger := seelog.Current + defer func() { + // Restore the original logger + err := seelog.ReplaceLogger(originalLogger) + if err != nil { + t.Errorf("Failed to restore original logger: %v", err) + } + }() + + tests := []struct { + name string + logLevel seelog.LogLevel + logFunc func(logger seelog.LoggerInterface, msg string) + outputFormat string + }{ + { + name: "trace", + logLevel: seelog.TraceLvl, + logFunc: func(l seelog.LoggerInterface, msg string) { l.Trace(msg) }, + outputFormat: jsonFmt, + }, + { + name: "debug", + logLevel: seelog.DebugLvl, + logFunc: func(l seelog.LoggerInterface, msg string) { l.Debug(msg) }, + outputFormat: logFmt, + }, + { + name: "info", + logLevel: seelog.InfoLvl, + logFunc: func(l seelog.LoggerInterface, msg string) { l.Info(msg) }, + outputFormat: "windows", + }, + { + name: "warn", + logLevel: seelog.WarnLvl, + logFunc: func(l seelog.LoggerInterface, msg string) { l.Warn(msg) }, + outputFormat: jsonFmt, + }, + { + name: "error", + logLevel: seelog.ErrorLvl, + logFunc: func(l seelog.LoggerInterface, msg string) { l.Error(msg) }, + outputFormat: logFmt, + }, + { + name: "critical", + logLevel: seelog.CriticalLvl, + logFunc: func(l seelog.LoggerInterface, msg string) { l.Critical(msg) }, + outputFormat: "windows", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockReceiver := &(mock_seelog.CustomLoggerReceiver{ + OutputFormat: tt.outputFormat, + }) + + SetCustomReceiver(mockReceiver) + + message := fmt.Sprintf("test %s message", tt.name) + logger := seelog.Current + tt.logFunc(logger, message) + + // Use a wait group to ensure we've processed the log + var wg sync.WaitGroup + wg.Add(1) + + go func() { + defer wg.Done() + for i := 0; i < 50; i++ { // try for up to 5 seconds + mockReceiver.Mu.Lock() + var called bool + var lastMessage string + + // Check the appropriate field based on log level + switch tt.logLevel { + case seelog.TraceLvl: + called = mockReceiver.TraceCalled + lastMessage = mockReceiver.LastTraceMessage + case seelog.DebugLvl: + called = mockReceiver.DebugCalled + lastMessage = mockReceiver.LastDebugMessage + case seelog.InfoLvl: + called = mockReceiver.InfoCalled + lastMessage = mockReceiver.LastInfoMessage + case seelog.WarnLvl: + called = mockReceiver.WarnCalled + lastMessage = mockReceiver.LastWarnMessage + case seelog.ErrorLvl: + called = mockReceiver.ErrorCalled + lastMessage = mockReceiver.LastErrorMessage + case seelog.CriticalLvl: + called = mockReceiver.CriticalCalled + lastMessage = mockReceiver.LastCriticalMessage + } + mockReceiver.Mu.Unlock() + + if called { + require.Contains(t, lastMessage, message) + return + } + time.Sleep(100 * time.Millisecond) + } + t.Errorf("%s method was not called within timeout", tt.name) + }() + + wg.Wait() + }) + } +} diff --git a/ecs-agent/logger/mocks/custom_logger_receiver.go b/ecs-agent/logger/mocks/custom_logger_receiver.go new file mode 100644 index 00000000000..450032d6356 --- /dev/null +++ b/ecs-agent/logger/mocks/custom_logger_receiver.go @@ -0,0 +1,99 @@ +package mock_seelog + +import ( + "sync" + + "github.com/cihub/seelog" +) + +type CustomLoggerReceiver struct { + OutputFormat string + Mu sync.Mutex + TraceCalled bool + LastTraceMessage string + DebugCalled bool + LastDebugMessage string + InfoCalled bool + LastInfoMessage string + WarnCalled bool + LastWarnMessage string + ErrorCalled bool + LastErrorMessage string + CriticalCalled bool + LastCriticalMessage string +} + +func (m *CustomLoggerReceiver) GetOutputFormat() string { + return m.OutputFormat +} + +func (m *CustomLoggerReceiver) Trace(message string) { + m.Mu.Lock() + defer m.Mu.Unlock() + m.TraceCalled = true + m.LastTraceMessage = message +} + +func (m *CustomLoggerReceiver) Debug(message string) { + m.Mu.Lock() + defer m.Mu.Unlock() + m.DebugCalled = true + m.LastDebugMessage = message +} + +func (m *CustomLoggerReceiver) Info(message string) { + m.Mu.Lock() + defer m.Mu.Unlock() + m.InfoCalled = true + m.LastInfoMessage = message +} + +func (m *CustomLoggerReceiver) Warn(message string) { + m.Mu.Lock() + defer m.Mu.Unlock() + m.WarnCalled = true + m.LastWarnMessage = message +} + +func (m *CustomLoggerReceiver) Error(message string) { + m.Mu.Lock() + defer m.Mu.Unlock() + m.ErrorCalled = true + m.LastErrorMessage = message +} + +func (m *CustomLoggerReceiver) Critical(message string) { + m.Mu.Lock() + defer m.Mu.Unlock() + m.CriticalCalled = true + m.LastCriticalMessage = message +} + +func (m *CustomLoggerReceiver) Flush() error { + return nil +} +func (m *CustomLoggerReceiver) AfterParse(args interface{}) error { + return nil +} + +func (m *CustomLoggerReceiver) Close() error { + return nil +} + +func (m *CustomLoggerReceiver) ReceiveMessage(message string, level seelog.LogLevel, + context seelog.LogContextInterface) error { + + switch level { + case seelog.DebugLvl: + m.Debug(message) + case seelog.InfoLvl: + m.Info(message) + case seelog.WarnLvl: + m.Warn(message) + case seelog.ErrorLvl: + m.Error(message) + case seelog.CriticalLvl: + m.Error(message) + } + return nil +} diff --git a/scripts/changelog/changelog.go b/scripts/changelog/changelog.go index 73ae8cbbbc2..1a4d4b9a05b 100644 --- a/scripts/changelog/changelog.go +++ b/scripts/changelog/changelog.go @@ -148,10 +148,11 @@ func getRPMChangeString(allChange []Change) string { // // amazon-ecs-init (1.36.0-1) stable; urgency=medium // -// * Cache Agent version 1.36.0 -// * capture a fixed tail of container logs when removing a container +// - Cache Agent version 1.36.0 // -// -- Cameron Sparr Wed, 08 Jan 2020 11:00:00 -0800 +// - capture a fixed tail of container logs when removing a container +// +// -- Cameron Sparr Wed, 08 Jan 2020 11:00:00 -0800 func getUbuntuChangeString(allChange []Change) string { result := "" for _, change := range allChange { @@ -171,9 +172,9 @@ func getUbuntuChangeString(allChange []Change) string { // ------------------------------------------------------------------- // Tue Apr 22 20:54:26 UTC 2013 - your@email.com // -// - level 1 bullet point; long descriptions -// should wrap -// - another l1 bullet point +// - level 1 bullet point; long descriptions +// should wrap +// - another l1 bullet point func getSuseChangeString(allChange []Change) string { result := "" for _, change := range allChange { @@ -190,9 +191,9 @@ func getSuseChangeString(allChange []Change) string { // format as follows // -// ## 1.35.0 -// * Cache Agent version 1.36.0 -// * capture a fixed tail of container logs when removing a container +// ## 1.35.0 +// * Cache Agent version 1.36.0 +// * capture a fixed tail of container logs when removing a container func getTopLevelChangeString(allChange []Change) string { result := "# Changelog\n\n" for _, change := range allChange {