maddy

Fork https://github.com/foxcpp/maddy

git clone git://git.lin.moe/go/maddy.git

  1/*
  2Maddy Mail Server - Composable all-in-one email server.
  3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
  4
  5This program is free software: you can redistribute it and/or modify
  6it under the terms of the GNU General Public License as published by
  7the Free Software Foundation, either version 3 of the License, or
  8(at your option) any later version.
  9
 10This program is distributed in the hope that it will be useful,
 11but WITHOUT ANY WARRANTY; without even the implied warranty of
 12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13GNU General Public License for more details.
 14
 15You should have received a copy of the GNU General Public License
 16along with this program.  If not, see <https://www.gnu.org/licenses/>.
 17*/
 18
 19// Package log implements a minimalistic logging library.
 20package log
 21
 22import (
 23	"fmt"
 24	"io"
 25	"os"
 26	"strings"
 27	"time"
 28
 29	"github.com/foxcpp/maddy/framework/exterrors"
 30	"go.uber.org/zap"
 31)
 32
 33// Logger is the structure that writes formatted output to the underlying
 34// log.Output object.
 35//
 36// Logger is stateless and can be copied freely.  However, consider that
 37// underlying log.Output will not be copied.
 38//
 39// Each log message is prefixed with logger name.  Timestamp and debug flag
 40// formatting is done by log.Output.
 41//
 42// No serialization is provided by Logger, its log.Output responsibility to
 43// ensure goroutine-safety if necessary.
 44type Logger struct {
 45	Out   Output
 46	Name  string
 47	Debug bool
 48
 49	// Additional fields that will be added
 50	// to the Msg output.
 51	Fields map[string]interface{}
 52}
 53
 54func (l Logger) Zap() *zap.Logger {
 55	// TODO: Migrate to using zap natively.
 56	return zap.New(zapLogger{L: l})
 57}
 58
 59func (l Logger) Debugf(format string, val ...interface{}) {
 60	if !l.Debug {
 61		return
 62	}
 63	l.log(true, l.formatMsg(fmt.Sprintf(format, val...), nil))
 64}
 65
 66func (l Logger) Debugln(val ...interface{}) {
 67	if !l.Debug {
 68		return
 69	}
 70	l.log(true, l.formatMsg(strings.TrimRight(fmt.Sprintln(val...), "\n"), nil))
 71}
 72
 73func (l Logger) Printf(format string, val ...interface{}) {
 74	l.log(false, l.formatMsg(fmt.Sprintf(format, val...), nil))
 75}
 76
 77func (l Logger) Println(val ...interface{}) {
 78	l.log(false, l.formatMsg(strings.TrimRight(fmt.Sprintln(val...), "\n"), nil))
 79}
 80
 81// Msg writes an event log message in a machine-readable format (currently
 82// JSON).
 83//
 84//	name: msg\t{"key":"value","key2":"value2"}
 85//
 86// Key-value pairs are built from fields slice which should contain key strings
 87// 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 the
 91// string returned by FormatLog method. Same goes for fmt.Stringer and error
 92// 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}
101
102// Error writes an event log message in a machine-readable format (currently
103// JSON) containing information about the error. If err does have a Fields
104// method that returns map[string]interface{}, its result will be added to the
105// message.
106//
107//	name: msg\t{"key":"value","key2":"value2"}
108//
109// Additionally, values from fields will be added to it, as handled by
110// Logger.Msg.
111//
112// In the context of Error method, "msg" typically indicates the top-level
113// context in which the error is *handled*. For example, if error leads to
114// 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		return
118	}
119
120	errFields := exterrors.Fields(err)
121	allFields := make(map[string]interface{}, len(fields)+len(errFields)+2)
122	for k, v := range errFields {
123		allFields[k] = v
124	}
125
126	// If there is already a 'reason' field - use it, it probably
127	// provides a better explanation than error text itself.
128	if allFields["reason"] == nil {
129		allFields["reason"] = err.Error()
130	}
131	fieldsToMap(fields, allFields)
132
133	l.log(false, l.formatMsg(msg, allFields))
134}
135
136func (l Logger) DebugMsg(kind string, fields ...interface{}) {
137	if !l.Debug {
138		return
139	}
140	m := make(map[string]interface{}, len(fields)/2)
141	fieldsToMap(fields, m)
142	l.log(true, l.formatMsg(kind, m))
143}
144
145func fieldsToMap(fields []interface{}, out map[string]interface{}) {
146	var lastKey string
147	for i, val := range fields {
148		if i%2 == 0 {
149			// Key
150			key, ok := val.(string)
151			if !ok {
152				// Misformatted arguments, attempt to provide useful message
153				// anyway.
154				out[fmt.Sprint("field", i)] = key
155				continue
156			}
157			lastKey = key
158		} else {
159			// Value
160			out[lastKey] = val
161		}
162	}
163}
164
165func (l Logger) formatMsg(msg string, fields map[string]interface{}) string {
166	formatted := strings.Builder{}
167
168	formatted.WriteString(msg)
169	formatted.WriteRune('\t')
170
171	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] = v
177		}
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	}
183
184	return formatted.String()
185}
186
187type LogFormatter interface {
188	FormatLog() string
189}
190
191// Write implements io.Writer, all bytes sent
192// 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), nil
197}
198
199// DebugWriter returns a writer that will act like Logger.Write
200// 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.Discard
205	}
206	l.Debug = true
207	return &l
208}
209
210func (l Logger) log(debug bool, s string) {
211	if l.Name != "" {
212		s = l.Name + ": " + s
213	}
214
215	if l.Out != nil {
216		l.Out.Write(time.Now(), debug, s)
217		return
218	}
219	if DefaultLogger.Out != nil {
220		DefaultLogger.Out.Write(time.Now(), debug, s)
221		return
222	}
223
224	// Logging is disabled - do nothing.
225}
226
227// DefaultLogger is the global Logger object that is used by
228// 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)}
233
234func 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...) }