1/*2Maddy Mail Server - Composable all-in-one email server.3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors45This program is free software: you can redistribute it and/or modify6it under the terms of the GNU General Public License as published by7the Free Software Foundation, either version 3 of the License, or8(at your option) any later version.910This program is distributed in the hope that it will be useful,11but WITHOUT ANY WARRANTY; without even the implied warranty of12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the13GNU General Public License for more details.1415You should have received a copy of the GNU General Public License16along with this program. If not, see <https://www.gnu.org/licenses/>.17*/1819// Package log implements a minimalistic logging library.20package log2122import (23 "fmt"24 "io"25 "os"26 "strings"27 "time"2829 "github.com/foxcpp/maddy/framework/exterrors"30 "go.uber.org/zap"31)3233// Logger is the structure that writes formatted output to the underlying34// log.Output object.35//36// Logger is stateless and can be copied freely. However, consider that37// underlying log.Output will not be copied.38//39// Each log message is prefixed with logger name. Timestamp and debug flag40// formatting is done by log.Output.41//42// No serialization is provided by Logger, its log.Output responsibility to43// ensure goroutine-safety if necessary.44type Logger struct {45 Out Output46 Name string47 Debug bool4849 // Additional fields that will be added50 // to the Msg output.51 Fields map[string]interface{}52}5354func (l Logger) Zap() *zap.Logger {55 // TODO: Migrate to using zap natively.56 return zap.New(zapLogger{L: l})57}5859func (l Logger) Debugf(format string, val ...interface{}) {60 if !l.Debug {61 return62 }63 l.log(true, l.formatMsg(fmt.Sprintf(format, val...), nil))64}6566func (l Logger) Debugln(val ...interface{}) {67 if !l.Debug {68 return69 }70 l.log(true, l.formatMsg(strings.TrimRight(fmt.Sprintln(val...), "\n"), nil))71}7273func (l Logger) Printf(format string, val ...interface{}) {74 l.log(false, l.formatMsg(fmt.Sprintf(format, val...), nil))75}7677func (l Logger) Println(val ...interface{}) {78 l.log(false, l.formatMsg(strings.TrimRight(fmt.Sprintln(val...), "\n"), nil))79}8081// Msg writes an event log message in a machine-readable format (currently82// JSON).83//84// name: msg\t{"key":"value","key2":"value2"}85//86// Key-value pairs are built from fields slice which should contain key strings87// followed by corresponding values. That is, for example, []interface{"key",88// "value", "key2", "value2"}.89//90// If value in fields implements LogFormatter, it will be represented by the91// string returned by FormatLog method. Same goes for fmt.Stringer and error92// interfaces.93//94// Additionally, time.Time is written as a string in ISO 8601 format.95// time.Duration follows fmt.Stringer rule above.96func (l Logger) Msg(msg string, fields ...interface{}) {97 m := make(map[string]interface{}, len(fields)/2)98 fieldsToMap(fields, m)99 l.log(false, l.formatMsg(msg, m))100}101102// Error writes an event log message in a machine-readable format (currently103// JSON) containing information about the error. If err does have a Fields104// method that returns map[string]interface{}, its result will be added to the105// message.106//107// name: msg\t{"key":"value","key2":"value2"}108//109// Additionally, values from fields will be added to it, as handled by110// Logger.Msg.111//112// In the context of Error method, "msg" typically indicates the top-level113// context in which the error is *handled*. For example, if error leads to114// rejection of SMTP DATA command, msg will probably be "DATA error".115func (l Logger) Error(msg string, err error, fields ...interface{}) {116 if err == nil {117 return118 }119120 errFields := exterrors.Fields(err)121 allFields := make(map[string]interface{}, len(fields)+len(errFields)+2)122 for k, v := range errFields {123 allFields[k] = v124 }125126 // If there is already a 'reason' field - use it, it probably127 // provides a better explanation than error text itself.128 if allFields["reason"] == nil {129 allFields["reason"] = err.Error()130 }131 fieldsToMap(fields, allFields)132133 l.log(false, l.formatMsg(msg, allFields))134}135136func (l Logger) DebugMsg(kind string, fields ...interface{}) {137 if !l.Debug {138 return139 }140 m := make(map[string]interface{}, len(fields)/2)141 fieldsToMap(fields, m)142 l.log(true, l.formatMsg(kind, m))143}144145func fieldsToMap(fields []interface{}, out map[string]interface{}) {146 var lastKey string147 for i, val := range fields {148 if i%2 == 0 {149 // Key150 key, ok := val.(string)151 if !ok {152 // Misformatted arguments, attempt to provide useful message153 // anyway.154 out[fmt.Sprint("field", i)] = key155 continue156 }157 lastKey = key158 } else {159 // Value160 out[lastKey] = val161 }162 }163}164165func (l Logger) formatMsg(msg string, fields map[string]interface{}) string {166 formatted := strings.Builder{}167168 formatted.WriteString(msg)169 formatted.WriteRune('\t')170171 if len(l.Fields)+len(fields) != 0 {172 if fields == nil {173 fields = make(map[string]interface{})174 }175 for k, v := range l.Fields {176 fields[k] = v177 }178 if err := marshalOrderedJSON(&formatted, fields); err != nil {179 // Fallback to printing the message with minimal processing.180 return fmt.Sprintf("[BROKEN FORMATTING: %v] %v %+v", err, msg, fields)181 }182 }183184 return formatted.String()185}186187type LogFormatter interface {188 FormatLog() string189}190191// Write implements io.Writer, all bytes sent192// to it will be written as a separate log messages.193// No line-buffering is done.194func (l Logger) Write(s []byte) (int, error) {195 l.log(false, strings.TrimRight(string(s), "\n"))196 return len(s), nil197}198199// DebugWriter returns a writer that will act like Logger.Write200// but will use debug flag on messages. If Logger.Debug is false,201// Write method of returned object will be no-op.202func (l Logger) DebugWriter() io.Writer {203 if !l.Debug {204 return io.Discard205 }206 l.Debug = true207 return &l208}209210func (l Logger) log(debug bool, s string) {211 if l.Name != "" {212 s = l.Name + ": " + s213 }214215 if l.Out != nil {216 l.Out.Write(time.Now(), debug, s)217 return218 }219 if DefaultLogger.Out != nil {220 DefaultLogger.Out.Write(time.Now(), debug, s)221 return222 }223224 // Logging is disabled - do nothing.225}226227// DefaultLogger is the global Logger object that is used by228// package-level logging functions.229//230// As with all other Loggers, it is not gorountine-safe on its own,231// however underlying log.Output may provide necessary serialization.232var DefaultLogger = Logger{Out: WriterOutput(os.Stderr, false)}233234func Debugf(format string, val ...interface{}) { DefaultLogger.Debugf(format, val...) }235func Debugln(val ...interface{}) { DefaultLogger.Debugln(val...) }236func Printf(format string, val ...interface{}) { DefaultLogger.Printf(format, val...) }237func Println(val ...interface{}) { DefaultLogger.Println(val...) }