cmd: configurable logrus formatters
We lost the nice context-stack [jobname][taskname][...] at the beginning of each log line when switching to logrus. Define some field names that define these contexts. Write a human-friendly formatter that presents these field names like the solution we had before logrus. Write some other formatters for logfmt and json output along the way. Limit ourselves to stdout logging for now.
This commit is contained in:
+105
-34
@@ -2,52 +2,123 @@ package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CLIFormatter struct {
|
||||
const (
|
||||
logJobField string = "job"
|
||||
logTaskField string = "task"
|
||||
logFSField string = "filesystem"
|
||||
logMapFromField string = "map_from"
|
||||
logMapToField string = "map_to"
|
||||
logIncFromField string = "inc_from"
|
||||
logIncToField string = "inc_to"
|
||||
)
|
||||
|
||||
type NoFormatter struct{}
|
||||
|
||||
func (f NoFormatter) Format(e *logrus.Entry) ([]byte, error) {
|
||||
return []byte(e.Message), nil
|
||||
}
|
||||
|
||||
func (f CLIFormatter) Format(e *logrus.Entry) (out []byte, err error) {
|
||||
var buf bytes.Buffer
|
||||
fmt.Fprintf(&buf, "%s\n", e.Message)
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
type HumanFormatter struct{}
|
||||
|
||||
var stdhookStderrLevels []logrus.Level = []logrus.Level{
|
||||
logrus.PanicLevel, logrus.FatalLevel, logrus.ErrorLevel, logrus.WarnLevel,
|
||||
}
|
||||
|
||||
type Stdhook struct {
|
||||
}
|
||||
|
||||
func NewStdHook() *Stdhook {
|
||||
return &Stdhook{}
|
||||
}
|
||||
|
||||
func (h *Stdhook) Levels() []logrus.Level {
|
||||
// Accept all so we can filter the output later
|
||||
return []logrus.Level{
|
||||
logrus.PanicLevel, logrus.FatalLevel, logrus.ErrorLevel, logrus.WarnLevel,
|
||||
logrus.InfoLevel, logrus.DebugLevel,
|
||||
func (f HumanFormatter) shortLevel(l logrus.Level) string {
|
||||
switch l {
|
||||
case logrus.DebugLevel:
|
||||
return "DBG"
|
||||
case logrus.InfoLevel:
|
||||
return "INF"
|
||||
case logrus.WarnLevel:
|
||||
return "WRN"
|
||||
case logrus.ErrorLevel:
|
||||
return "ERR"
|
||||
case logrus.PanicLevel:
|
||||
return "PNC"
|
||||
}
|
||||
panic("incomplete implementation")
|
||||
}
|
||||
|
||||
func (h *Stdhook) Fire(entry *logrus.Entry) error {
|
||||
s, err := entry.String()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, l := range stdhookStderrLevels {
|
||||
if l == entry.Level {
|
||||
fmt.Fprint(os.Stderr, s)
|
||||
return nil
|
||||
func (f HumanFormatter) Format(e *logrus.Entry) (out []byte, err error) {
|
||||
|
||||
var line bytes.Buffer
|
||||
|
||||
fmt.Fprintf(&line, "[%s]", f.shortLevel(e.Level))
|
||||
|
||||
prefixFields := []string{logJobField, logTaskField, logFSField}
|
||||
prefixed := make(map[string]bool, len(prefixFields)+2)
|
||||
for _, field := range prefixFields {
|
||||
val, ok := e.Data[field].(string)
|
||||
if ok {
|
||||
fmt.Fprintf(&line, "[%s]", val)
|
||||
prefixed[field] = true
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
fmt.Fprint(os.Stdout, s)
|
||||
return nil
|
||||
// even more prefix fields
|
||||
mapFrom, mapFromOk := e.Data[logMapFromField].(string)
|
||||
mapTo, mapToOk := e.Data[logMapToField].(string)
|
||||
if mapFromOk && mapToOk {
|
||||
fmt.Fprintf(&line, "[%s => %s]", mapFrom, mapTo)
|
||||
prefixed[logMapFromField], prefixed[logMapToField] = true, true
|
||||
}
|
||||
incFrom, incFromOk := e.Data[logIncFromField].(string)
|
||||
incTo, incToOk := e.Data[logIncToField].(string)
|
||||
if incFromOk && incToOk {
|
||||
fmt.Fprintf(&line, "[%s => %s]", incFrom, incTo)
|
||||
prefixed[logIncFromField], prefixed[logIncToField] = true, true
|
||||
}
|
||||
|
||||
fmt.Fprintf(&line, ": %s", e.Message)
|
||||
|
||||
for field, value := range e.Data {
|
||||
|
||||
if prefixed[field] {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.ContainsAny(field, " \t") {
|
||||
return nil, errors.Errorf("field must not contain whitespace: '%s'", field)
|
||||
}
|
||||
fmt.Fprintf(&line, " %s=\"%s\"", field, value)
|
||||
}
|
||||
|
||||
fmt.Fprintf(&line, "\n")
|
||||
|
||||
return line.Bytes(), nil
|
||||
}
|
||||
|
||||
type JSONFormatter struct{}
|
||||
|
||||
func (f JSONFormatter) Format(e *logrus.Entry) ([]byte, error) {
|
||||
data := make(logrus.Fields, len(e.Data)+3)
|
||||
for k, v := range e.Data {
|
||||
switch v := v.(type) {
|
||||
case error:
|
||||
// Otherwise errors are ignored by `encoding/json`
|
||||
// https://github.com/sirupsen/logrus/issues/137
|
||||
data[k] = v.Error()
|
||||
default:
|
||||
_, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("field is not JSON encodable: %s", k)
|
||||
}
|
||||
data[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
data["msg"] = e.Message
|
||||
data["time"] = e.Time.Format(time.RFC3339)
|
||||
data["level"] = e.Level
|
||||
|
||||
return json.Marshal(data)
|
||||
|
||||
}
|
||||
|
||||
type nopWriter int
|
||||
|
||||
Reference in New Issue
Block a user