2017-03-06 04:05:33 +01:00
|
|
|
// Copyright (c) 2017 Daniel Oaks <daniel@danieloaks.net>
|
|
|
|
// released under the MIT license
|
|
|
|
|
2017-03-10 13:02:08 +01:00
|
|
|
package logger
|
2017-03-06 04:05:33 +01:00
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"fmt"
|
|
|
|
"os"
|
|
|
|
"time"
|
2017-03-06 07:28:38 +01:00
|
|
|
|
2017-03-06 13:11:10 +01:00
|
|
|
"strings"
|
|
|
|
|
2017-03-10 03:27:05 +01:00
|
|
|
"sync"
|
|
|
|
|
|
|
|
colorable "github.com/mattn/go-colorable"
|
2017-03-06 07:28:38 +01:00
|
|
|
"github.com/mgutz/ansi"
|
2017-03-06 04:05:33 +01:00
|
|
|
)
|
|
|
|
|
2017-03-10 13:02:08 +01:00
|
|
|
// Level represents the level to log messages at.
|
|
|
|
type Level int
|
2017-03-06 04:05:33 +01:00
|
|
|
|
|
|
|
const (
|
|
|
|
// LogDebug represents debug messages.
|
2017-03-10 13:02:08 +01:00
|
|
|
LogDebug Level = iota
|
2017-03-06 04:05:33 +01:00
|
|
|
// LogInfo represents informational messages.
|
|
|
|
LogInfo
|
2017-03-10 13:02:08 +01:00
|
|
|
// LogWarning represents warnings.
|
|
|
|
LogWarning
|
2017-03-06 04:05:33 +01:00
|
|
|
// LogError represents errors.
|
|
|
|
LogError
|
|
|
|
)
|
|
|
|
|
2017-03-06 04:31:10 +01:00
|
|
|
var (
|
2017-06-19 22:53:16 +02:00
|
|
|
// LogLevelNames takes a config name and gives the real log level.
|
2017-03-10 13:02:08 +01:00
|
|
|
LogLevelNames = map[string]Level{
|
2017-03-06 04:31:10 +01:00
|
|
|
"debug": LogDebug,
|
|
|
|
"info": LogInfo,
|
2017-03-10 13:02:08 +01:00
|
|
|
"warn": LogWarning,
|
|
|
|
"warning": LogWarning,
|
|
|
|
"warnings": LogWarning,
|
2017-03-06 04:31:10 +01:00
|
|
|
"error": LogError,
|
|
|
|
"errors": LogError,
|
|
|
|
}
|
2017-06-19 22:53:16 +02:00
|
|
|
// LogLevelDisplayNames gives the display name to use for our log levels.
|
2017-03-10 13:02:08 +01:00
|
|
|
LogLevelDisplayNames = map[Level]string{
|
|
|
|
LogDebug: "debug",
|
|
|
|
LogInfo: "info",
|
|
|
|
LogWarning: "warning",
|
|
|
|
LogError: "error",
|
2017-03-06 06:50:23 +01:00
|
|
|
}
|
2017-03-06 04:31:10 +01:00
|
|
|
)
|
|
|
|
|
2017-03-10 13:02:08 +01:00
|
|
|
// Manager is the main interface used to log debug/info/error messages.
|
|
|
|
type Manager struct {
|
2017-10-02 05:31:40 +02:00
|
|
|
configMutex sync.RWMutex
|
2017-03-10 13:02:08 +01:00
|
|
|
loggers []singleLogger
|
2017-05-01 10:51:37 +02:00
|
|
|
stdoutWriteLock sync.Mutex // use one lock for both stdout and stderr
|
2017-03-26 13:41:52 +02:00
|
|
|
fileWriteLock sync.Mutex
|
2017-10-04 19:41:19 +02:00
|
|
|
loggingRawIO bool
|
2017-03-06 04:05:33 +01:00
|
|
|
}
|
|
|
|
|
2017-10-06 04:44:11 +02:00
|
|
|
// LoggingConfig represents the configuration of a single logger.
|
2017-10-02 05:31:40 +02:00
|
|
|
type LoggingConfig struct {
|
|
|
|
Method string
|
|
|
|
MethodStdout bool
|
|
|
|
MethodStderr bool
|
|
|
|
MethodFile bool
|
|
|
|
Filename string
|
|
|
|
TypeString string `yaml:"type"`
|
|
|
|
Types []string `yaml:"real-types"`
|
|
|
|
ExcludedTypes []string `yaml:"real-excluded-types"`
|
|
|
|
LevelString string `yaml:"level"`
|
|
|
|
Level Level `yaml:"level-real"`
|
2017-03-10 13:02:08 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewManager returns a new log manager.
|
2017-10-02 05:31:40 +02:00
|
|
|
func NewManager(config []LoggingConfig) (*Manager, error) {
|
2017-03-10 13:02:08 +01:00
|
|
|
var logger Manager
|
2017-03-06 06:16:00 +01:00
|
|
|
|
2017-10-02 05:31:40 +02:00
|
|
|
if err := logger.ApplyConfig(config); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &logger, nil
|
|
|
|
}
|
|
|
|
|
2017-10-06 04:44:11 +02:00
|
|
|
// ApplyConfig applies the given config to this logger (rehashes the config, in other words).
|
2017-10-02 05:31:40 +02:00
|
|
|
func (logger *Manager) ApplyConfig(config []LoggingConfig) error {
|
|
|
|
logger.configMutex.Lock()
|
|
|
|
defer logger.configMutex.Unlock()
|
|
|
|
|
|
|
|
for _, logger := range logger.loggers {
|
|
|
|
logger.Close()
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.loggers = nil
|
2017-10-04 19:41:19 +02:00
|
|
|
logger.loggingRawIO = false
|
2017-10-02 05:31:40 +02:00
|
|
|
|
|
|
|
// for safety, this deep-copies all mutable data in `config`
|
|
|
|
// XXX let's keep it that way
|
|
|
|
var lastErr error
|
2017-03-06 06:16:00 +01:00
|
|
|
for _, logConfig := range config {
|
2017-03-10 13:02:08 +01:00
|
|
|
typeMap := make(map[string]bool)
|
|
|
|
for _, name := range logConfig.Types {
|
|
|
|
typeMap[name] = true
|
|
|
|
}
|
|
|
|
excludedTypeMap := make(map[string]bool)
|
|
|
|
for _, name := range logConfig.ExcludedTypes {
|
|
|
|
excludedTypeMap[name] = true
|
|
|
|
}
|
|
|
|
|
|
|
|
sLogger := singleLogger{
|
2017-05-01 10:51:37 +02:00
|
|
|
MethodSTDOUT: logConfig.MethodStdout,
|
2017-03-10 13:02:08 +01:00
|
|
|
MethodSTDERR: logConfig.MethodStderr,
|
2017-03-06 06:16:00 +01:00
|
|
|
MethodFile: fileMethod{
|
2017-03-10 13:02:08 +01:00
|
|
|
Enabled: logConfig.MethodFile,
|
2017-03-06 06:16:00 +01:00
|
|
|
Filename: logConfig.Filename,
|
|
|
|
},
|
2017-03-10 03:27:05 +01:00
|
|
|
Level: logConfig.Level,
|
2017-03-10 13:02:08 +01:00
|
|
|
Types: typeMap,
|
|
|
|
ExcludedTypes: excludedTypeMap,
|
2017-05-01 10:51:37 +02:00
|
|
|
stdoutWriteLock: &logger.stdoutWriteLock,
|
2017-03-26 13:41:52 +02:00
|
|
|
fileWriteLock: &logger.fileWriteLock,
|
2017-03-06 06:16:00 +01:00
|
|
|
}
|
2017-03-10 13:02:08 +01:00
|
|
|
if typeMap["userinput"] || typeMap["useroutput"] || (typeMap["*"] && !(excludedTypeMap["userinput"] && excludedTypeMap["useroutput"])) {
|
2017-10-04 19:41:19 +02:00
|
|
|
logger.loggingRawIO = true
|
2017-03-08 12:57:31 +01:00
|
|
|
}
|
2017-03-06 06:16:00 +01:00
|
|
|
if sLogger.MethodFile.Enabled {
|
2017-03-06 06:50:23 +01:00
|
|
|
file, err := os.OpenFile(sLogger.MethodFile.Filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666)
|
2017-03-06 06:16:00 +01:00
|
|
|
if err != nil {
|
2017-10-02 05:31:40 +02:00
|
|
|
lastErr = fmt.Errorf("Could not open log file %s [%s]", sLogger.MethodFile.Filename, err.Error())
|
2017-03-06 06:16:00 +01:00
|
|
|
}
|
|
|
|
writer := bufio.NewWriter(file)
|
|
|
|
sLogger.MethodFile.File = file
|
|
|
|
sLogger.MethodFile.Writer = writer
|
|
|
|
}
|
2017-03-06 06:50:23 +01:00
|
|
|
logger.loggers = append(logger.loggers, sLogger)
|
2017-03-06 06:16:00 +01:00
|
|
|
}
|
|
|
|
|
2017-10-02 05:31:40 +02:00
|
|
|
return lastErr
|
|
|
|
}
|
|
|
|
|
2017-10-06 04:44:11 +02:00
|
|
|
// IsLoggingRawIO returns true if raw user input and output is being logged.
|
2017-10-04 19:41:19 +02:00
|
|
|
func (logger *Manager) IsLoggingRawIO() bool {
|
2017-10-02 05:31:40 +02:00
|
|
|
logger.configMutex.RLock()
|
|
|
|
defer logger.configMutex.RUnlock()
|
2017-10-04 19:41:19 +02:00
|
|
|
return logger.loggingRawIO
|
2017-03-06 04:05:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Log logs the given message with the given details.
|
2017-03-10 13:02:08 +01:00
|
|
|
func (logger *Manager) Log(level Level, logType string, messageParts ...string) {
|
2017-10-02 05:31:40 +02:00
|
|
|
logger.configMutex.RLock()
|
|
|
|
defer logger.configMutex.RUnlock()
|
|
|
|
|
2017-03-06 04:05:33 +01:00
|
|
|
for _, singleLogger := range logger.loggers {
|
2017-03-06 11:15:28 +01:00
|
|
|
singleLogger.Log(level, logType, messageParts...)
|
2017-03-06 04:05:33 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-10 13:02:08 +01:00
|
|
|
// Debug logs the given message as a debug message.
|
|
|
|
func (logger *Manager) Debug(logType string, messageParts ...string) {
|
2017-10-02 05:31:40 +02:00
|
|
|
logger.Log(LogDebug, logType, messageParts...)
|
2017-03-10 13:02:08 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Info logs the given message as an info message.
|
|
|
|
func (logger *Manager) Info(logType string, messageParts ...string) {
|
2017-10-02 05:31:40 +02:00
|
|
|
logger.Log(LogInfo, logType, messageParts...)
|
2017-03-10 13:02:08 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Warning logs the given message as a warning message.
|
|
|
|
func (logger *Manager) Warning(logType string, messageParts ...string) {
|
2017-10-02 05:31:40 +02:00
|
|
|
logger.Log(LogWarning, logType, messageParts...)
|
2017-03-10 13:02:08 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Error logs the given message as an error message.
|
|
|
|
func (logger *Manager) Error(logType string, messageParts ...string) {
|
2017-10-02 05:31:40 +02:00
|
|
|
logger.Log(LogError, logType, messageParts...)
|
2017-03-10 13:02:08 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Fatal logs the given message as an error message, then exits.
|
|
|
|
func (logger *Manager) Fatal(logType string, messageParts ...string) {
|
|
|
|
logger.Error(logType, messageParts...)
|
|
|
|
logger.Error("FATAL", "Fatal error encountered, application exiting")
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
2017-03-06 06:16:00 +01:00
|
|
|
type fileMethod struct {
|
|
|
|
Enabled bool
|
|
|
|
Filename string
|
|
|
|
File *os.File
|
|
|
|
Writer *bufio.Writer
|
|
|
|
}
|
|
|
|
|
2017-03-10 13:02:08 +01:00
|
|
|
// singleLogger represents a single logger instance.
|
|
|
|
type singleLogger struct {
|
2017-05-01 10:51:37 +02:00
|
|
|
stdoutWriteLock *sync.Mutex
|
2017-03-26 13:41:52 +02:00
|
|
|
fileWriteLock *sync.Mutex
|
2017-05-01 10:51:37 +02:00
|
|
|
MethodSTDOUT bool
|
2017-03-10 03:27:05 +01:00
|
|
|
MethodSTDERR bool
|
|
|
|
MethodFile fileMethod
|
2017-03-10 13:02:08 +01:00
|
|
|
Level Level
|
2017-03-10 03:27:05 +01:00
|
|
|
Types map[string]bool
|
|
|
|
ExcludedTypes map[string]bool
|
2017-03-06 04:05:33 +01:00
|
|
|
}
|
|
|
|
|
2017-10-02 05:31:40 +02:00
|
|
|
func (logger *singleLogger) Close() error {
|
|
|
|
if logger.MethodFile.Enabled {
|
|
|
|
flushErr := logger.MethodFile.Writer.Flush()
|
|
|
|
closeErr := logger.MethodFile.File.Close()
|
|
|
|
if flushErr != nil {
|
|
|
|
return flushErr
|
|
|
|
}
|
|
|
|
return closeErr
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-03-06 04:05:33 +01:00
|
|
|
// Log logs the given message with the given details.
|
2017-03-10 13:02:08 +01:00
|
|
|
func (logger *singleLogger) Log(level Level, logType string, messageParts ...string) {
|
2017-03-06 04:05:33 +01:00
|
|
|
// no logging enabled
|
2017-05-01 10:51:37 +02:00
|
|
|
if !(logger.MethodSTDOUT || logger.MethodSTDERR || logger.MethodFile.Enabled) {
|
2017-03-06 04:05:33 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// ensure we're logging to the given level
|
|
|
|
if level < logger.Level {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// ensure we're capturing this logType
|
2017-03-06 13:11:10 +01:00
|
|
|
logTypeCleaned := strings.ToLower(strings.TrimSpace(logType))
|
|
|
|
capturing := (logger.Types["*"] || logger.Types[logTypeCleaned]) && !logger.ExcludedTypes["*"] && !logger.ExcludedTypes[logTypeCleaned]
|
2017-03-06 04:05:33 +01:00
|
|
|
if !capturing {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// assemble full line
|
2017-03-06 11:15:28 +01:00
|
|
|
timeGrey := ansi.ColorFunc("243")
|
2017-03-06 07:28:38 +01:00
|
|
|
grey := ansi.ColorFunc("8")
|
|
|
|
alert := ansi.ColorFunc("232+b:red")
|
|
|
|
warn := ansi.ColorFunc("black:214")
|
2017-03-06 11:15:28 +01:00
|
|
|
info := ansi.ColorFunc("117")
|
2017-03-06 07:28:38 +01:00
|
|
|
debug := ansi.ColorFunc("78")
|
2017-03-06 11:15:28 +01:00
|
|
|
section := ansi.ColorFunc("229")
|
2017-03-06 07:28:38 +01:00
|
|
|
|
2017-03-10 13:02:08 +01:00
|
|
|
levelDisplay := LogLevelDisplayNames[level]
|
2017-03-06 07:28:38 +01:00
|
|
|
if level == LogError {
|
|
|
|
levelDisplay = alert(levelDisplay)
|
2017-03-10 13:02:08 +01:00
|
|
|
} else if level == LogWarning {
|
2017-03-06 07:28:38 +01:00
|
|
|
levelDisplay = warn(levelDisplay)
|
|
|
|
} else if level == LogInfo {
|
|
|
|
levelDisplay = info(levelDisplay)
|
|
|
|
} else if level == LogDebug {
|
|
|
|
levelDisplay = debug(levelDisplay)
|
|
|
|
}
|
|
|
|
|
2017-03-06 11:15:28 +01:00
|
|
|
sep := grey(":")
|
|
|
|
fullStringFormatted := fmt.Sprintf("%s %s %s %s %s %s ", timeGrey(time.Now().UTC().Format("2006-01-02T15:04:05Z")), sep, levelDisplay, sep, section(logType), sep)
|
2017-03-13 14:52:28 +01:00
|
|
|
fullStringRaw := fmt.Sprintf("%s : %s : %s : ", time.Now().UTC().Format("2006-01-02T15:04:05Z"), LogLevelDisplayNames[level], logType)
|
2017-03-06 11:15:28 +01:00
|
|
|
for i, p := range messageParts {
|
|
|
|
fullStringFormatted += p
|
|
|
|
fullStringRaw += p
|
|
|
|
if i != len(messageParts)-1 {
|
|
|
|
fullStringFormatted += " " + sep + " "
|
|
|
|
fullStringRaw += " : "
|
|
|
|
}
|
|
|
|
}
|
2017-03-06 04:05:33 +01:00
|
|
|
|
|
|
|
// output
|
2017-05-01 10:51:37 +02:00
|
|
|
if logger.MethodSTDOUT {
|
|
|
|
logger.stdoutWriteLock.Lock()
|
|
|
|
fmt.Fprintln(colorable.NewColorableStdout(), fullStringFormatted)
|
|
|
|
logger.stdoutWriteLock.Unlock()
|
|
|
|
}
|
2017-03-06 04:05:33 +01:00
|
|
|
if logger.MethodSTDERR {
|
2017-05-01 10:51:37 +02:00
|
|
|
logger.stdoutWriteLock.Lock()
|
2017-03-10 03:27:05 +01:00
|
|
|
fmt.Fprintln(colorable.NewColorableStderr(), fullStringFormatted)
|
2017-05-01 10:51:37 +02:00
|
|
|
logger.stdoutWriteLock.Unlock()
|
2017-03-06 04:05:33 +01:00
|
|
|
}
|
|
|
|
if logger.MethodFile.Enabled {
|
2017-03-26 13:41:52 +02:00
|
|
|
logger.fileWriteLock.Lock()
|
2017-03-06 11:15:28 +01:00
|
|
|
logger.MethodFile.Writer.WriteString(fullStringRaw + "\n")
|
2017-05-01 11:02:49 +02:00
|
|
|
logger.MethodFile.Writer.Flush()
|
2017-03-26 13:41:52 +02:00
|
|
|
logger.fileWriteLock.Unlock()
|
2017-03-06 04:05:33 +01:00
|
|
|
}
|
|
|
|
}
|