ecs-agent/logger/log.go (354 lines of code) (raw):
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.
package logger
import (
"fmt"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/cihub/seelog"
)
const (
LOGLEVEL_ENV_VAR = "ECS_LOGLEVEL"
LOGLEVEL_ON_INSTANCE_ENV_VAR = "ECS_LOGLEVEL_ON_INSTANCE"
LOGFILE_ENV_VAR = "ECS_LOGFILE"
LOG_DRIVER_ENV_VAR = "ECS_LOG_DRIVER"
LOG_ROLLOVER_TYPE_ENV_VAR = "ECS_LOG_ROLLOVER_TYPE"
LOG_OUTPUT_FORMAT_ENV_VAR = "ECS_LOG_OUTPUT_FORMAT"
LOG_MAX_FILE_SIZE_ENV_VAR = "ECS_LOG_MAX_FILE_SIZE_MB"
LOG_MAX_ROLL_COUNT_ENV_VAR = "ECS_LOG_MAX_ROLL_COUNT"
logFmt = "logfmt"
jsonFmt = "json"
DEFAULT_LOGLEVEL = "info"
DEFAULT_LOGLEVEL_WHEN_DRIVER_SET = "off"
DEFAULT_ROLLOVER_TYPE = "date"
DEFAULT_OUTPUT_FORMAT = logFmt
DEFAULT_TIMESTAMP_FORMAT = time.RFC3339
DEFAULT_MAX_FILE_SIZE float64 = 10
DEFAULT_MAX_ROLL_COUNT int = 24
DEFAULT_LOGTO_STDOUT = true
)
// 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
// logLevels is the mapping from ECS_LOGLEVEL to Seelog provided levels.
var logLevels = map[string]string{
"debug": "debug",
"info": "info",
"warn": "warn",
"error": "error",
"crit": "critical",
"none": "off",
}
type logConfig struct {
RolloverType string
MaxRollCount int
MaxFileSizeMB float64
logfile string
driverLevel string
instanceLevel string
outputFormat string
logToStdout bool
lock sync.Mutex
}
var Config *logConfig
func ecsMsgFormatter(params string) seelog.FormatterFunc {
return func(message string, level seelog.LogLevel, context seelog.LogContextInterface) interface{} {
buf := bufferPool.Get()
defer bufferPool.Put(buf)
// temporary measure to make this change backwards compatible as we update to structured logs
if strings.HasPrefix(message, structuredTxtFormatPrefix) {
message = strings.TrimPrefix(message, structuredTxtFormatPrefix)
buf.WriteString(message)
} else if strings.HasPrefix(message, structuredJsonFormatPrefix) {
message = strings.TrimPrefix(message, structuredJsonFormatPrefix)
message = strings.TrimRight(message, ",")
buf.WriteByte('{')
buf.WriteString(message)
buf.WriteByte('}')
} else {
buf.WriteString(message)
}
return buf.String()
}
}
func logfmtFormatter(params string) seelog.FormatterFunc {
return func(message string, level seelog.LogLevel, context seelog.LogContextInterface) interface{} {
buf := bufferPool.Get()
defer bufferPool.Put(buf)
buf.WriteString("level=")
buf.WriteString(level.String())
buf.WriteByte(' ')
buf.WriteString("time=")
buf.WriteString(context.CallTime().UTC().Format(timestampFormat))
buf.WriteByte(' ')
// temporary measure to make this change backwards compatible as we update to structured logs
if strings.HasPrefix(message, structuredTxtFormatPrefix) {
message = strings.TrimPrefix(message, structuredTxtFormatPrefix)
buf.WriteString(message)
} else {
buf.WriteString("msg=")
buf.WriteString(fmt.Sprintf("%q", message))
buf.WriteByte(' ')
buf.WriteString("module=")
buf.WriteString(context.FileName())
}
buf.WriteByte('\n')
return buf.String()
}
}
func jsonFormatter(params string) seelog.FormatterFunc {
return func(message string, level seelog.LogLevel, context seelog.LogContextInterface) interface{} {
buf := bufferPool.Get()
defer bufferPool.Put(buf)
buf.WriteString(`{"level":"`)
buf.WriteString(level.String())
buf.WriteString(`","time":"`)
buf.WriteString(context.CallTime().UTC().Format(timestampFormat))
buf.WriteString(`",`)
// temporary measure to make this change backwards compatible as we update to structured logs
if strings.HasPrefix(message, structuredJsonFormatPrefix) {
message = strings.TrimPrefix(message, structuredJsonFormatPrefix)
message = strings.TrimRight(message, ",")
buf.WriteString(message)
buf.WriteByte('}')
} else {
buf.WriteString(`"msg":`)
buf.WriteString(fmt.Sprintf("%q", message))
buf.WriteString(`,"module":"`)
buf.WriteString(context.FileName())
buf.WriteString(`"}`)
}
buf.WriteByte('\n')
return buf.String()
}
}
func reloadConfig() {
logger, err := seelog.LoggerFromConfigAsString(seelogConfig())
if err != nil {
seelog.Error(err)
return
}
setGlobalLogger(logger, Config.outputFormat)
}
func seelogConfig() string {
driverLogChildren := []string{}
platformLogConfig := platformLogConfig()
if Config.logToStdout {
driverLogChildren = append(driverLogChildren, ` <console />`)
}
if platformLogConfig != "" {
driverLogChildren = append(driverLogChildren, platformLogConfig)
}
c := `
<seelog type="asyncloop">
<outputs formatid="` + Config.outputFormat + `">`
if len(driverLogChildren) > 0 {
c += `
<filter levels="` + getLevelList(Config.driverLevel) + `">
`
c += strings.Join(driverLogChildren, "\n")
c += `
</filter>`
}
if Config.logfile != "" {
c += `
<filter levels="` + getLevelList(Config.instanceLevel) + `">`
if Config.RolloverType == "size" {
c += `
<rollingfile filename="` + Config.logfile + `" type="size"
maxsize="` + strconv.Itoa(int(Config.MaxFileSizeMB*1000000)) + `" archivetype="none" maxrolls="` + strconv.Itoa(Config.MaxRollCount) + `" />`
} else if Config.RolloverType == "none" {
c += `
<file path="` + Config.logfile + `"/>`
} else {
c += `
<rollingfile filename="` + Config.logfile + `" type="date"
datepattern="2006-01-02-15" archivetype="none" maxrolls="` + strconv.Itoa(Config.MaxRollCount) + `" />`
}
c += `
</filter>`
}
c += `
</outputs>
<formats>
<format id="` + logFmt + `" format="%EcsAgentLogfmt" />
<format id="` + jsonFmt + `" format="%EcsAgentJson" />
<format id="windows" format="%EcsMsg" />
</formats>
</seelog>`
return c
}
func getLevelList(fileLevel string) string {
levelLists := map[string]string{
"debug": "debug,info,warn,error,critical",
"info": "info,warn,error,critical",
"warn": "warn,error,critical",
"error": "error,critical",
"critical": "critical",
"off": "off",
}
return levelLists[fileLevel]
}
// GetLevel gets the log level
func GetLevel() string {
Config.lock.Lock()
defer Config.lock.Unlock()
return Config.driverLevel
}
func setInstanceLevelDefault() string {
if logDriver := os.Getenv(LOG_DRIVER_ENV_VAR); logDriver != "" {
return DEFAULT_LOGLEVEL_WHEN_DRIVER_SET
}
if loglevel := os.Getenv(LOGLEVEL_ENV_VAR); loglevel != "" {
return loglevel
}
return DEFAULT_LOGLEVEL
}
// SetInstanceLogLevel explicitly sets the log level for instance logs.
func SetInstanceLogLevel(instanceLogLevel string) {
parsedLevel, ok := logLevels[strings.ToLower(instanceLogLevel)]
if ok {
Config.lock.Lock()
defer Config.lock.Unlock()
Config.instanceLevel = parsedLevel
reloadConfig()
} else {
seelog.Error("Instance log level mapping not found")
}
}
// SetDriverLogLevel explicitly sets the log level for a custom driver.
func SetDriverLogLevel(driverLogLevel string) {
parsedLevel, ok := logLevels[strings.ToLower(driverLogLevel)]
if ok {
Config.lock.Lock()
defer Config.lock.Unlock()
Config.driverLevel = parsedLevel
reloadConfig()
} else {
seelog.Error("Driver log level mapping not found")
}
}
// SetConfigLogFile sets the default output file of the logger.
func SetConfigLogFile(logFile string) {
if logFile != "" {
Config.lock.Lock()
defer Config.lock.Unlock()
Config.logfile = logFile
reloadConfig()
} else {
seelog.Error("Cannot use empty log file")
}
}
// SetConfigOutputFormat sets the output format of the logger.
// e.g. json, xml, etc.
func SetConfigOutputFormat(outputFormat string) {
Config.lock.Lock()
defer Config.lock.Unlock()
Config.outputFormat = outputFormat
reloadConfig()
}
// SetConfigMaxFileSizeMB sets the max file size of a log file
// in Megabytes before the logger rotates to a new file.
func SetConfigMaxFileSizeMB(maxSizeInMB float64) {
if maxSizeInMB > 0 {
Config.lock.Lock()
defer Config.lock.Unlock()
Config.MaxFileSizeMB = maxSizeInMB
reloadConfig()
} else {
seelog.Error("Invalid Max File Size Provided")
}
}
// SetRolloverType sets the logging rollover constraint.
// This should be either size or date. Logger will roll
// to a new log file based on this constraint.
func SetRolloverType(rolloverType string) {
if rolloverType == "date" || rolloverType == "size" || rolloverType == "none" {
Config.lock.Lock()
defer Config.lock.Unlock()
Config.RolloverType = rolloverType
reloadConfig()
} else {
seelog.Error("Invalid log rollover type provided")
}
}
// 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 != "" {
timestampFormat = format
}
}
// SetLogToStdout decides whether the logger
// should write to stdout using the <console/> tag
// in addition to logfiles that are set up.
func SetLogToStdout(duplicate bool) {
Config.lock.Lock()
defer Config.lock.Unlock()
Config.logToStdout = duplicate
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 := `
<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),
driverLevel: DEFAULT_LOGLEVEL,
instanceLevel: setInstanceLevelDefault(),
RolloverType: DEFAULT_ROLLOVER_TYPE,
outputFormat: DEFAULT_OUTPUT_FORMAT,
MaxFileSizeMB: DEFAULT_MAX_FILE_SIZE,
MaxRollCount: DEFAULT_MAX_ROLL_COUNT,
logToStdout: DEFAULT_LOGTO_STDOUT,
}
}
func registerCustomFormatters() {
if err := seelog.RegisterCustomFormatter("EcsAgentLogfmt", logfmtFormatter); err != nil {
seelog.Error(err)
}
if err := seelog.RegisterCustomFormatter("EcsAgentJson", jsonFormatter); err != nil {
seelog.Error(err)
}
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)
}
if InstanceLogLevel := os.Getenv(LOGLEVEL_ON_INSTANCE_ENV_VAR); InstanceLogLevel != "" {
SetInstanceLogLevel(InstanceLogLevel)
}
if RolloverType := os.Getenv(LOG_ROLLOVER_TYPE_ENV_VAR); RolloverType != "" {
Config.RolloverType = RolloverType
}
if outputFormat := os.Getenv(LOG_OUTPUT_FORMAT_ENV_VAR); outputFormat != "" {
Config.outputFormat = outputFormat
}
if MaxRollCount := os.Getenv(LOG_MAX_ROLL_COUNT_ENV_VAR); MaxRollCount != "" {
i, err := strconv.Atoi(MaxRollCount)
if err == nil {
Config.MaxRollCount = i
} else {
seelog.Error("Invalid value for "+LOG_MAX_ROLL_COUNT_ENV_VAR, err)
}
}
if MaxFileSizeMB := os.Getenv(LOG_MAX_FILE_SIZE_ENV_VAR); MaxFileSizeMB != "" {
f, err := strconv.ParseFloat(MaxFileSizeMB, 64)
if err == nil {
Config.MaxFileSizeMB = f
} else {
seelog.Error("Invalid value for "+LOG_MAX_FILE_SIZE_ENV_VAR, err)
}
}
registerPlatformLogger()
reloadConfig()
}