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 {