internal/apmlog/logger.go (165 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License 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 apmlog
import (
"fmt"
"io"
"log"
"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 (
loggerMu sync.RWMutex
defaultLogger *LevelLogger
fastjsonPool = &sync.Pool{
New: func() interface{} {
return &fastjson.Writer{}
},
}
)
// DefaultLogger initialises defaultLogger using the environment variables
// ELASTIC_APM_LOG_FILE and ELASTIC_APM_LOG_LEVEL. If defaultLogger is non-nil,
// it returns the logger.
func DefaultLogger() *LevelLogger {
loggerMu.RLock()
if defaultLogger != nil {
defer loggerMu.RUnlock()
return defaultLogger
}
loggerMu.RUnlock()
loggerMu.Lock()
defer loggerMu.Unlock()
// Releasing the RLock could allow another goroutine to call
// SetDefaultLogger() before trying to take the write Lock; double
// check that DefaultLogger is still nil after acquiring the write
// lock.
if defaultLogger != nil {
return defaultLogger
}
fileStr := strings.TrimSpace(os.Getenv(EnvLogFile))
if fileStr == "" {
return defaultLogger
}
var logWriter io.Writer
switch strings.ToLower(fileStr) {
case "stdout":
logWriter = os.Stdout
case "stderr":
logWriter = os.Stderr
default:
f, err := os.Create(fileStr)
if err != nil {
log.Printf("failed to create %q: %s (disabling logging)", fileStr, err)
return nil
}
logWriter = &syncFile{File: f}
}
logLevel := DefaultLevel
if levelStr := strings.TrimSpace(os.Getenv(EnvLogLevel)); levelStr != "" {
level, err := ParseLogLevel(levelStr)
if err != nil {
log.Printf("invalid %s %q, falling back to %q", EnvLogLevel, levelStr, logLevel)
} else {
logLevel = level
}
}
defaultLogger = &LevelLogger{w: logWriter, level: logLevel}
return defaultLogger
}
// SetDefaultLogger sets the package default logger to the logger provided.
func SetDefaultLogger(l *LevelLogger) {
loggerMu.Lock()
defer loggerMu.Unlock()
defaultLogger = l
}
// Log levels.
const (
TraceLevel Level = iota
DebugLevel
InfoLevel
WarningLevel
ErrorLevel
CriticalLevel
OffLevel
)
// Level represents a log level.
type Level uint32
func (l Level) String() string {
switch l {
case TraceLevel:
return "trace"
case DebugLevel:
return "debug"
case InfoLevel:
return "info"
case WarningLevel:
return "warning"
case ErrorLevel:
return "error"
case CriticalLevel:
return "critical"
case OffLevel:
return "off"
}
return ""
}
// ParseLogLevel parses s as a log level.
func ParseLogLevel(s string) (Level, error) {
switch strings.ToLower(s) {
case "trace":
return TraceLevel, nil
case "debug":
return DebugLevel, nil
case "info":
return InfoLevel, nil
case "warn", "warning":
// "warn" exists for backwards compatibility;
// "warning" is the canonical level name.
return WarningLevel, nil
case "error":
return ErrorLevel, nil
case "critical":
return CriticalLevel, nil
case "off":
return OffLevel, nil
}
return OffLevel, fmt.Errorf("invalid log level string %q", s)
}
// 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
}
// 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...)
}
// Errorf logs a message with log.Printf, with an ERROR prefix.
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(WarningLevel, format, args...)
}
func (l *LevelLogger) logf(level Level, format string, args ...interface{}) {
if level < l.Level() {
return
}
jw := fastjsonPool.Get().(*fastjson.Writer)
jw.RawString(`{"level":"`)
jw.RawString(level.String())
jw.RawString(`","time":"`)
jw.Time(time.Now(), time.RFC3339)
jw.RawString(`","message":`)
jw.String(fmt.Sprintf(format, args...))
jw.RawString("}\n")
l.w.Write(jw.Bytes())
jw.Reset()
fastjsonPool.Put(jw)
}
type syncFile struct {
mu sync.Mutex
*os.File
}
// Write calls f.File.Write with f.mu held, to protect multiple Tracers
// in the same process from one another.
func (f *syncFile) Write(data []byte) (int, error) {
f.mu.Lock()
defer f.mu.Unlock()
return f.File.Write(data)
}