-
Notifications
You must be signed in to change notification settings - Fork 198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make log_level
centrally configurable
#859
Changes from all commits
b07a745
3f1699f
ee0ccf6
52d67fa
179f2b8
0ec5c78
9636bdb
4e2e909
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,14 +24,26 @@ import ( | |
"os" | ||
"strings" | ||
"sync" | ||
"sync/atomic" | ||
"time" | ||
|
||
"go.elastic.co/fastjson" | ||
) | ||
|
||
const ( | ||
// EnvLogFile is the environment variable that controls where the default logger writes. | ||
EnvLogFile = "ELASTIC_APM_LOG_FILE" | ||
|
||
// EnvLogLevel is the environment variable that controls the default logger's level. | ||
EnvLogLevel = "ELASTIC_APM_LOG_LEVEL" | ||
|
||
// DefaultLevel holds the default log level, if EnvLogLevel is not specified. | ||
DefaultLevel Level = ErrorLevel | ||
) | ||
|
||
var ( | ||
// DefaultLogger is the default Logger to use, if ELASTIC_APM_LOG_* are specified. | ||
DefaultLogger Logger | ||
DefaultLogger *LevelLogger | ||
|
||
fastjsonPool = &sync.Pool{ | ||
New: func() interface{} { | ||
|
@@ -41,12 +53,15 @@ var ( | |
) | ||
|
||
func init() { | ||
initDefaultLogger() | ||
InitDefaultLogger() | ||
} | ||
|
||
func initDefaultLogger() { | ||
fileStr := strings.TrimSpace(os.Getenv("ELASTIC_APM_LOG_FILE")) | ||
// InitDefaultLogger initialises DefaultLogger using the environment variables | ||
// ELASTIC_APM_LOG_FILE and ELASTIC_APM_LOG_LEVEL. | ||
func InitDefaultLogger() { | ||
fileStr := strings.TrimSpace(os.Getenv(EnvLogFile)) | ||
if fileStr == "" { | ||
DefaultLogger = nil | ||
return | ||
} | ||
|
||
|
@@ -65,85 +80,98 @@ func initDefaultLogger() { | |
logWriter = &syncFile{File: f} | ||
} | ||
|
||
logLevel := errorLevel | ||
if levelStr := strings.TrimSpace(os.Getenv("ELASTIC_APM_LOG_LEVEL")); levelStr != "" { | ||
level, err := parseLogLevel(levelStr) | ||
logLevel := DefaultLevel | ||
if levelStr := strings.TrimSpace(os.Getenv(EnvLogLevel)); levelStr != "" { | ||
level, err := ParseLogLevel(levelStr) | ||
if err != nil { | ||
log.Printf("invalid ELASTIC_APM_LOG_LEVEL %q, falling back to %q", levelStr, logLevel) | ||
log.Printf("invalid %s %q, falling back to %q", EnvLogLevel, levelStr, logLevel) | ||
} else { | ||
logLevel = level | ||
} | ||
} | ||
DefaultLogger = levelLogger{w: logWriter, level: logLevel} | ||
DefaultLogger = &LevelLogger{w: logWriter, level: logLevel} | ||
} | ||
|
||
// Log levels. | ||
const ( | ||
debugLevel logLevel = iota | ||
infoLevel | ||
warnLevel | ||
errorLevel | ||
noLevel | ||
DebugLevel Level = iota | ||
InfoLevel | ||
WarnLevel | ||
ErrorLevel | ||
CriticalLevel | ||
OffLevel | ||
) | ||
|
||
type logLevel uint8 | ||
// Level represents a log level. | ||
type Level uint32 | ||
|
||
func (l logLevel) String() string { | ||
func (l Level) String() string { | ||
switch l { | ||
case debugLevel: | ||
case DebugLevel: | ||
return "debug" | ||
case infoLevel: | ||
case InfoLevel: | ||
return "info" | ||
case warnLevel: | ||
case WarnLevel: | ||
return "warn" | ||
case errorLevel: | ||
case ErrorLevel: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice catch, thanks! |
||
return "error" | ||
} | ||
return "" | ||
} | ||
|
||
func parseLogLevel(s string) (logLevel, error) { | ||
// ParseLogLevel parses s as a log level. | ||
func ParseLogLevel(s string) (Level, error) { | ||
switch strings.ToLower(s) { | ||
case "debug": | ||
return debugLevel, nil | ||
return DebugLevel, nil | ||
case "info": | ||
return infoLevel, nil | ||
return InfoLevel, nil | ||
case "warn": | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (This is my first time looking at the go agent, and really reviewing much Go code at all, so feel free to ignore if I'm missing something obvious.) The spec uses "warning" rather than "warn". Is this function parsing level strings from central config? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @trentm Thanks, nice pick up! You're quite right, I missed that. I'll open another PR to fix it. |
||
return warnLevel, nil | ||
return WarnLevel, nil | ||
case "error": | ||
return errorLevel, nil | ||
return ErrorLevel, nil | ||
case "critical": | ||
return CriticalLevel, nil | ||
case "off": | ||
return OffLevel, nil | ||
} | ||
return noLevel, fmt.Errorf("invalid log level string %q", s) | ||
return OffLevel, fmt.Errorf("invalid log level string %q", s) | ||
} | ||
|
||
// Logger provides methods for logging. | ||
type Logger interface { | ||
Debugf(format string, args ...interface{}) | ||
Errorf(format string, args ...interface{}) | ||
Warningf(format string, args ...interface{}) | ||
// LevelLogger is a level logging implementation that will log to a file, | ||
// stdout, or stderr. The level may be updated dynamically via SetLevel. | ||
type LevelLogger struct { | ||
level Level // should be accessed with sync/atomic | ||
w io.Writer | ||
} | ||
|
||
type levelLogger struct { | ||
w io.Writer | ||
level logLevel | ||
// Level returns the current logging level. | ||
func (l *LevelLogger) Level() Level { | ||
return Level(atomic.LoadUint32((*uint32)(&l.level))) | ||
} | ||
|
||
// SetLevel sets level as the minimum logging level. | ||
func (l *LevelLogger) SetLevel(level Level) { | ||
atomic.StoreUint32((*uint32)(&l.level), uint32(level)) | ||
} | ||
|
||
// Debugf logs a message with log.Printf, with a DEBUG prefix. | ||
func (l levelLogger) Debugf(format string, args ...interface{}) { | ||
l.logf(debugLevel, format, args...) | ||
func (l *LevelLogger) Debugf(format string, args ...interface{}) { | ||
l.logf(DebugLevel, format, args...) | ||
} | ||
|
||
// Errorf logs a message with log.Printf, with an ERROR prefix. | ||
func (l levelLogger) Errorf(format string, args ...interface{}) { | ||
l.logf(errorLevel, format, args...) | ||
func (l *LevelLogger) Errorf(format string, args ...interface{}) { | ||
l.logf(ErrorLevel, format, args...) | ||
} | ||
|
||
// Warningf logs a message with log.Printf, with a WARNING prefix. | ||
func (l levelLogger) Warningf(format string, args ...interface{}) { | ||
l.logf(warnLevel, format, args...) | ||
func (l *LevelLogger) Warningf(format string, args ...interface{}) { | ||
l.logf(WarnLevel, format, args...) | ||
} | ||
|
||
func (l levelLogger) logf(level logLevel, format string, args ...interface{}) { | ||
if level < l.level { | ||
func (l *LevelLogger) logf(level Level, format string, args ...interface{}) { | ||
if level < l.Level() { | ||
return | ||
} | ||
jw := fastjsonPool.Get().(*fastjson.Writer) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might be missing something - but are there any tests for actually applying the remote log level and also falling back to the local config if a remote config is no longer provided?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
testTracerCentralConfigUpdate
(which this test calls) does that inapm-agent-go/config_test.go
Lines 142 to 165 in 879c34a
Each of these sub-tests defines a config key, value to set remotely, and a predicate that indicates whether the remote config has been applied. The code block I pointed to first checks that with the initial tracer config the predicate returns false; then sets the remote config and checks the predicate returns true; then clears out all remote config and again checks that the predicate returns false.