Skip to content

Commit

Permalink
feat: introduce customreceiver in logging
Browse files Browse the repository at this point in the history
  • Loading branch information
niyatim23 committed Feb 21, 2025
1 parent c1d7359 commit 0f6e1a9
Show file tree
Hide file tree
Showing 7 changed files with 491 additions and 23 deletions.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

62 changes: 62 additions & 0 deletions ecs-agent/logger/custom_receiver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package logger

import (
"fmt"

"github.com/cihub/seelog"
)

// CustomReceiver defines the interface for custom logging implementations
type CustomReceiver interface {
GetTimestampFormat() string
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.Println("Unhandled level: ", 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.Println("Couldn't flush the logger due to: ", err)
}
}

func (w *customReceiverWrapper) Close() error {
return w.receiver.Close()
}
74 changes: 67 additions & 7 deletions ecs-agent/logger/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ const (
// Because timestamp format will be called in the custom formatter
// for each log message processed, it should not be handled
// with an explicitly write protected configuration.
var timestampFormat = DEFAULT_TIMESTAMP_FORMAT
var (
timestampFormatMu sync.RWMutex
timestampFormat = DEFAULT_TIMESTAMP_FORMAT
)

// logLevels is the mapping from ECS_LOGLEVEL to Seelog provided levels.
var logLevels = map[string]string{
Expand Down Expand Up @@ -105,7 +108,7 @@ func logfmtFormatter(params string) seelog.FormatterFunc {
buf.WriteString(level.String())
buf.WriteByte(' ')
buf.WriteString("time=")
buf.WriteString(context.CallTime().UTC().Format(timestampFormat))
buf.WriteString(context.CallTime().UTC().Format(getTimestampFormat()))
buf.WriteByte(' ')
// temporary measure to make this change backwards compatible as we update to structured logs
if strings.HasPrefix(message, structuredTxtFormatPrefix) {
Expand All @@ -130,7 +133,7 @@ func jsonFormatter(params string) seelog.FormatterFunc {
buf.WriteString(`{"level":"`)
buf.WriteString(level.String())
buf.WriteString(`","time":"`)
buf.WriteString(context.CallTime().UTC().Format(timestampFormat))
buf.WriteString(context.CallTime().UTC().Format(getTimestampFormat()))
buf.WriteString(`",`)
// temporary measure to make this change backwards compatible as we update to structured logs
if strings.HasPrefix(message, structuredJsonFormatPrefix) {
Expand Down Expand Up @@ -318,13 +321,22 @@ func SetRolloverType(rolloverType string) {
}
}

// Add getter for timestampFormat
func getTimestampFormat() string {
timestampFormatMu.RLock()
defer timestampFormatMu.RUnlock()
return timestampFormat
}

// SetTimestampFormat sets the time formatting
// for custom seelog formatters. It will expect
// a valid time format such as time.RFC3339
// or "2006-01-02T15:04:05.000".
func SetTimestampFormat(format string) {
if format != "" {
timestampFormatMu.Lock()
timestampFormat = format
timestampFormatMu.Unlock()
}
}

Expand All @@ -339,6 +351,50 @@ 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}
SetTimestampFormat(receiver.GetTimestampFormat())
outputFormat := receiver.GetOutputFormat()

// Internal seelog configuration
customConfig := `
<seelog type="asyncloop">
<outputs>
<custom name="customReceiver" formatid="` + outputFormat + `"/>
</outputs>
<formats>
<format id="` + logFmt + `" format="%EcsAgentLogfmt" />
<format id="` + jsonFmt + `" format="%EcsAgentJson" />
<format id="windows" format="%EcsMsg" />
</formats>
</seelog>
`

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),
Expand All @@ -352,10 +408,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)
}
Expand All @@ -365,6 +418,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)
Expand Down
Loading

0 comments on commit 0f6e1a9

Please sign in to comment.