ergo/irc/logger/logger.go

302 lines
8.2 KiB
Go
Raw Normal View History

2017-03-06 04:05:33 +01:00
// Copyright (c) 2017 Daniel Oaks <daniel@danieloaks.net>
// released under the MIT license
package logger
2017-03-06 04:05:33 +01:00
import (
"bufio"
2018-12-31 17:37:58 +01:00
"bytes"
2017-03-06 04:05:33 +01:00
"fmt"
"os"
"time"
2017-03-06 07:28:38 +01:00
"strings"
"sync"
2018-05-01 12:00:09 +02:00
"sync/atomic"
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
)
// Level represents the level to log messages at.
type Level int
2017-03-06 04:05:33 +01:00
const (
// LogDebug represents debug messages.
LogDebug Level = iota
2017-03-06 04:05:33 +01:00
// LogInfo represents informational messages.
LogInfo
// LogWarning represents warnings.
LogWarning
2017-03-06 04:05:33 +01:00
// LogError represents errors.
LogError
)
2018-12-31 17:37:58 +01:00
var (
colorTimeGrey = ansi.ColorFunc("243")
colorGrey = ansi.ColorFunc("8")
colorAlert = ansi.ColorFunc("232+b:red")
colorWarn = ansi.ColorFunc("black:214")
colorInfo = ansi.ColorFunc("117")
colorDebug = ansi.ColorFunc("78")
colorSection = ansi.ColorFunc("229")
separator = colorGrey(":")
colorableStdout = colorable.NewColorableStdout()
colorableStderr = colorable.NewColorableStderr()
)
2017-03-06 04:31:10 +01:00
var (
// LogLevelNames takes a config name and gives the real log level.
LogLevelNames = map[string]Level{
2017-03-06 04:31:10 +01:00
"debug": LogDebug,
"info": LogInfo,
"warn": LogWarning,
"warning": LogWarning,
"warnings": LogWarning,
2017-03-06 04:31:10 +01:00
"error": LogError,
"errors": LogError,
}
// LogLevelDisplayNames gives the display name to use for our log levels.
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
)
// 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
loggers []singleLogger
2017-05-01 10:51:37 +02:00
stdoutWriteLock sync.Mutex // use one lock for both stdout and stderr
fileWriteLock sync.Mutex
2018-05-01 12:00:09 +02:00
loggingRawIO uint32
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"`
}
// NewManager returns a new log manager.
2017-10-02 05:31:40 +02:00
func NewManager(config []LoggingConfig) (*Manager, error) {
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
2018-05-01 12:00:09 +02:00
atomic.StoreUint32(&logger.loggingRawIO, 0)
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 {
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,
MethodSTDERR: logConfig.MethodStderr,
2017-03-06 06:16:00 +01:00
MethodFile: fileMethod{
Enabled: logConfig.MethodFile,
2017-03-06 06:16:00 +01:00
Filename: logConfig.Filename,
},
Level: logConfig.Level,
Types: typeMap,
ExcludedTypes: excludedTypeMap,
2017-05-01 10:51:37 +02:00
stdoutWriteLock: &logger.stdoutWriteLock,
fileWriteLock: &logger.fileWriteLock,
2017-03-06 06:16:00 +01:00
}
ioEnabled := typeMap["userinput"] || typeMap["useroutput"] || (typeMap["*"] && !(excludedTypeMap["userinput"] && excludedTypeMap["useroutput"]))
// raw I/O is only logged at level debug;
if ioEnabled && logConfig.Level == LogDebug {
2018-05-01 12:00:09 +02:00
atomic.StoreUint32(&logger.loggingRawIO, 1)
}
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 {
2018-05-01 12:00:09 +02:00
return atomic.LoadUint32(&logger.loggingRawIO) == 1
2017-03-06 04:05:33 +01:00
}
// Log logs the given message with the given details.
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
}
}
// 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...)
}
// 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...)
}
// 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...)
}
// 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-06 06:16:00 +01:00
type fileMethod struct {
Enabled bool
Filename string
File *os.File
Writer *bufio.Writer
}
// singleLogger represents a single logger instance.
type singleLogger struct {
2017-05-01 10:51:37 +02:00
stdoutWriteLock *sync.Mutex
fileWriteLock *sync.Mutex
2017-05-01 10:51:37 +02:00
MethodSTDOUT bool
MethodSTDERR bool
MethodFile fileMethod
Level Level
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.
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
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 07:28:38 +01:00
levelDisplay := LogLevelDisplayNames[level]
2017-03-06 07:28:38 +01:00
if level == LogError {
2018-12-31 17:37:58 +01:00
levelDisplay = colorAlert(levelDisplay)
} else if level == LogWarning {
2018-12-31 17:37:58 +01:00
levelDisplay = colorWarn(levelDisplay)
2017-03-06 07:28:38 +01:00
} else if level == LogInfo {
2018-12-31 17:37:58 +01:00
levelDisplay = colorInfo(levelDisplay)
2017-03-06 07:28:38 +01:00
} else if level == LogDebug {
2018-12-31 17:37:58 +01:00
levelDisplay = colorDebug(levelDisplay)
2017-03-06 07:28:38 +01:00
}
2018-12-31 17:37:58 +01:00
var formattedBuf, rawBuf bytes.Buffer
fmt.Fprintf(&formattedBuf, "%s %s %s %s %s %s ", colorTimeGrey(time.Now().UTC().Format("2006-01-02T15:04:05.000Z")), separator, levelDisplay, separator, colorSection(logType), separator)
if logger.MethodFile.Enabled {
fmt.Fprintf(&rawBuf, "%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 {
2018-12-31 17:37:58 +01:00
formattedBuf.WriteString(p)
if logger.MethodFile.Enabled {
rawBuf.WriteString(p)
}
2017-03-06 11:15:28 +01:00
if i != len(messageParts)-1 {
2018-12-31 17:37:58 +01:00
formattedBuf.WriteRune(' ')
formattedBuf.WriteString(separator)
formattedBuf.WriteRune(' ')
if logger.MethodFile.Enabled {
rawBuf.WriteString(" : ")
}
2017-03-06 11:15:28 +01:00
}
}
2018-12-31 17:37:58 +01:00
formattedBuf.WriteRune('\n')
if logger.MethodFile.Enabled {
rawBuf.WriteRune('\n')
}
2017-03-06 04:05:33 +01:00
// output
2017-05-01 10:51:37 +02:00
if logger.MethodSTDOUT {
logger.stdoutWriteLock.Lock()
2018-12-31 17:37:58 +01:00
colorableStdout.Write(formattedBuf.Bytes())
2017-05-01 10:51:37 +02:00
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()
2018-12-31 17:37:58 +01:00
colorableStderr.Write(formattedBuf.Bytes())
2017-05-01 10:51:37 +02:00
logger.stdoutWriteLock.Unlock()
2017-03-06 04:05:33 +01:00
}
if logger.MethodFile.Enabled {
logger.fileWriteLock.Lock()
2018-12-31 17:37:58 +01:00
logger.MethodFile.Writer.Write(rawBuf.Bytes())
logger.MethodFile.Writer.Flush()
logger.fileWriteLock.Unlock()
2017-03-06 04:05:33 +01:00
}
}