boulder/log/log.go

361 lines
9.5 KiB
Go

package log
import (
"encoding/base64"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"hash/crc32"
"io"
"log/syslog"
"os"
"strings"
"sync"
"github.com/jmhodges/clock"
"golang.org/x/term"
"github.com/letsencrypt/boulder/core"
)
// A Logger logs messages with explicit priority levels. It is
// implemented by a logging back-end as provided by New() or
// NewMock(). Any additions to this interface with format strings should be
// added to the govet configuration in .golangci.yml
type Logger interface {
Err(msg string)
Errf(format string, a ...interface{})
Warning(msg string)
Warningf(format string, a ...interface{})
Info(msg string)
Infof(format string, a ...interface{})
InfoObject(string, interface{})
Debug(msg string)
Debugf(format string, a ...interface{})
AuditInfo(msg string)
AuditInfof(format string, a ...interface{})
AuditObject(string, interface{})
AuditErr(string)
AuditErrf(format string, a ...interface{})
}
// impl implements Logger.
type impl struct {
w writer
}
// singleton defines the object of a Singleton pattern
type singleton struct {
once sync.Once
log Logger
}
// _Singleton is the single impl entity in memory
var _Singleton singleton
// The constant used to identify audit-specific messages
const auditTag = "[AUDIT]"
// New returns a new Logger that uses the given syslog.Writer as a backend
// and also writes to stdout/stderr. It is safe for concurrent use.
func New(log *syslog.Writer, stdoutLogLevel int, syslogLogLevel int) (Logger, error) {
if log == nil {
return nil, errors.New("Attempted to use a nil System Logger")
}
return &impl{
&bothWriter{
sync.Mutex{},
log,
newStdoutWriter(stdoutLogLevel),
syslogLogLevel,
},
}, nil
}
// StdoutLogger returns a Logger that writes solely to stdout and stderr.
// It is safe for concurrent use.
func StdoutLogger(level int) Logger {
return &impl{newStdoutWriter(level)}
}
func newStdoutWriter(level int) *stdoutWriter {
prefix, clkFormat := getPrefix()
return &stdoutWriter{
prefix: prefix,
level: level,
clkFormat: clkFormat,
clk: clock.New(),
stdout: os.Stdout,
stderr: os.Stderr,
isatty: term.IsTerminal(int(os.Stdout.Fd())),
}
}
// initialize is used in unit tests and called by `Get` before the logger
// is fully set up.
func initialize() {
const defaultPriority = syslog.LOG_INFO | syslog.LOG_LOCAL0
syslogger, err := syslog.Dial("", "", defaultPriority, "test")
if err != nil {
panic(err)
}
logger, err := New(syslogger, int(syslog.LOG_DEBUG), int(syslog.LOG_DEBUG))
if err != nil {
panic(err)
}
_ = Set(logger)
}
// Set configures the singleton Logger. This method
// must only be called once, and before calling Get the
// first time.
func Set(logger Logger) (err error) {
if _Singleton.log != nil {
err = errors.New("You may not call Set after it has already been implicitly or explicitly set")
_Singleton.log.Warning(err.Error())
} else {
_Singleton.log = logger
}
return
}
// Get obtains the singleton Logger. If Set has not been called first, this
// method initializes with basic defaults. The basic defaults cannot error, and
// subsequent access to an already-set Logger also cannot error, so this method is
// error-safe.
func Get() Logger {
_Singleton.once.Do(func() {
if _Singleton.log == nil {
initialize()
}
})
return _Singleton.log
}
type writer interface {
logAtLevel(syslog.Priority, string, ...interface{})
}
// bothWriter implements writer and writes to both syslog and stdout.
type bothWriter struct {
sync.Mutex
*syslog.Writer
*stdoutWriter
syslogLevel int
}
// stdoutWriter implements writer and writes just to stdout.
type stdoutWriter struct {
// prefix is a set of information that is the same for every log line,
// imitating what syslog emits for us when we use the syslog writer.
prefix string
level int
clkFormat string
clk clock.Clock
stdout io.Writer
stderr io.Writer
isatty bool
}
func LogLineChecksum(line string) string {
crc := crc32.ChecksumIEEE([]byte(line))
// Using the hash.Hash32 doesn't make this any easier
// as it also returns a uint32 rather than []byte
buf := make([]byte, binary.MaxVarintLen32)
binary.PutUvarint(buf, uint64(crc))
return base64.RawURLEncoding.EncodeToString(buf)
}
func checkSummed(msg string) string {
return fmt.Sprintf("%s %s", LogLineChecksum(msg), msg)
}
// logAtLevel logs the provided message at the appropriate level, writing to
// both stdout and the Logger
func (w *bothWriter) logAtLevel(level syslog.Priority, msg string, a ...interface{}) {
var err error
// Apply conditional formatting for f functions
if a != nil {
msg = fmt.Sprintf(msg, a...)
}
// Since messages are delimited by newlines, we have to escape any internal or
// trailing newlines before generating the checksum or outputting the message.
msg = strings.Replace(msg, "\n", "\\n", -1)
w.Lock()
defer w.Unlock()
switch syslogAllowed := int(level) <= w.syslogLevel; level {
case syslog.LOG_ERR:
if syslogAllowed {
err = w.Err(checkSummed(msg))
}
case syslog.LOG_WARNING:
if syslogAllowed {
err = w.Warning(checkSummed(msg))
}
case syslog.LOG_INFO:
if syslogAllowed {
err = w.Info(checkSummed(msg))
}
case syslog.LOG_DEBUG:
if syslogAllowed {
err = w.Debug(checkSummed(msg))
}
default:
err = w.Err(fmt.Sprintf("%s (unknown logging level: %d)", checkSummed(msg), int(level)))
}
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to write to syslog: %d %s (%s)\n", int(level), checkSummed(msg), err)
}
w.stdoutWriter.logAtLevel(level, msg)
}
// logAtLevel logs the provided message to stdout, or stderr if it is at Warning or Error level.
func (w *stdoutWriter) logAtLevel(level syslog.Priority, msg string, a ...interface{}) {
if int(level) <= w.level {
output := w.stdout
if int(level) <= int(syslog.LOG_WARNING) {
output = w.stderr
}
// Apply conditional formatting for f functions
if a != nil {
msg = fmt.Sprintf(msg, a...)
}
msg = strings.Replace(msg, "\n", "\\n", -1)
var color string
var reset string
const red = "\033[31m\033[1m"
const yellow = "\033[33m"
const gray = "\033[37m\033[2m"
if w.isatty {
if int(level) == int(syslog.LOG_DEBUG) {
color = gray
reset = "\033[0m"
} else if int(level) == int(syslog.LOG_WARNING) {
color = yellow
reset = "\033[0m"
} else if int(level) <= int(syslog.LOG_ERR) {
color = red
reset = "\033[0m"
}
}
if _, err := fmt.Fprintf(output, "%s%s %s%d %s %s%s\n",
color,
w.clk.Now().UTC().Format(w.clkFormat),
w.prefix,
int(level),
core.Command(),
checkSummed(msg),
reset); err != nil {
panic(fmt.Sprintf("failed to write to stdout: %v\n", err))
}
}
}
func (log *impl) auditAtLevel(level syslog.Priority, msg string, a ...interface{}) {
msg = fmt.Sprintf("%s %s", auditTag, msg)
log.w.logAtLevel(level, msg, a...)
}
// Err level messages are always marked with the audit tag, for special handling
// at the upstream system logger.
func (log *impl) Err(msg string) {
log.Errf(msg)
}
// Errf level messages are always marked with the audit tag, for special handling
// at the upstream system logger.
func (log *impl) Errf(format string, a ...interface{}) {
log.auditAtLevel(syslog.LOG_ERR, format, a...)
}
// Warning level messages pass through normally.
func (log *impl) Warning(msg string) {
log.Warningf(msg)
}
// Warningf level messages pass through normally.
func (log *impl) Warningf(format string, a ...interface{}) {
log.w.logAtLevel(syslog.LOG_WARNING, format, a...)
}
// Info level messages pass through normally.
func (log *impl) Info(msg string) {
log.Infof(msg)
}
// Infof level messages pass through normally.
func (log *impl) Infof(format string, a ...interface{}) {
log.w.logAtLevel(syslog.LOG_INFO, format, a...)
}
// InfoObject logs an INFO level JSON-serialized object message.
func (log *impl) InfoObject(msg string, obj interface{}) {
jsonObj, err := json.Marshal(obj)
if err != nil {
log.auditAtLevel(syslog.LOG_ERR, fmt.Sprintf("Object for msg %q could not be serialized to JSON. Raw: %+v", msg, obj))
return
}
log.Infof("%s JSON=%s", msg, jsonObj)
}
// Debug level messages pass through normally.
func (log *impl) Debug(msg string) {
log.Debugf(msg)
}
// Debugf level messages pass through normally.
func (log *impl) Debugf(format string, a ...interface{}) {
log.w.logAtLevel(syslog.LOG_DEBUG, format, a...)
}
// AuditInfo sends an INFO-severity message that is prefixed with the
// audit tag, for special handling at the upstream system logger.
func (log *impl) AuditInfo(msg string) {
log.AuditInfof(msg)
}
// AuditInfof sends an INFO-severity message that is prefixed with the
// audit tag, for special handling at the upstream system logger.
func (log *impl) AuditInfof(format string, a ...interface{}) {
log.auditAtLevel(syslog.LOG_INFO, format, a...)
}
// AuditObject sends an INFO-severity JSON-serialized object message that is prefixed
// with the audit tag, for special handling at the upstream system logger.
func (log *impl) AuditObject(msg string, obj interface{}) {
jsonObj, err := json.Marshal(obj)
if err != nil {
log.auditAtLevel(syslog.LOG_ERR, fmt.Sprintf("Object for msg %q could not be serialized to JSON. Raw: %+v", msg, obj))
return
}
log.auditAtLevel(syslog.LOG_INFO, fmt.Sprintf("%s JSON=%s", msg, jsonObj))
}
// AuditErr can format an error for auditing; it does so at ERR level.
func (log *impl) AuditErr(msg string) {
log.AuditErrf(msg)
}
// AuditErrf can format an error for auditing; it does so at ERR level.
func (log *impl) AuditErrf(format string, a ...interface{}) {
log.auditAtLevel(syslog.LOG_ERR, format, a...)
}