1package logger 2 3import ( 4 "fmt" 5 "io" 6 "time" 7 8 "github.com/jsternberg/zap-logfmt" 9 isatty "github.com/mattn/go-isatty" 10 "go.uber.org/zap" 11 "go.uber.org/zap/zapcore" 12) 13 14const TimeFormat = "2006-01-02T15:04:05.000000Z07:00" 15 16func New(w io.Writer) *zap.Logger { 17 config := NewConfig() 18 l, _ := config.New(w) 19 return l 20} 21 22func (c *Config) New(defaultOutput io.Writer) (*zap.Logger, error) { 23 w := defaultOutput 24 format := c.Format 25 if format == "console" { 26 // Disallow the console logger if the output is not a terminal. 27 return nil, fmt.Errorf("unknown logging format: %s", format) 28 } 29 30 // If the format is empty or auto, then set the format depending 31 // on whether or not a terminal is present. 32 if format == "" || format == "auto" { 33 if IsTerminal(w) { 34 format = "console" 35 } else { 36 format = "logfmt" 37 } 38 } 39 40 encoder, err := newEncoder(format) 41 if err != nil { 42 return nil, err 43 } 44 return zap.New(zapcore.NewCore( 45 encoder, 46 zapcore.Lock(zapcore.AddSync(w)), 47 c.Level, 48 ), zap.Fields(zap.String("log_id", nextID()))), nil 49} 50 51func newEncoder(format string) (zapcore.Encoder, error) { 52 config := newEncoderConfig() 53 switch format { 54 case "json": 55 return zapcore.NewJSONEncoder(config), nil 56 case "console": 57 return zapcore.NewConsoleEncoder(config), nil 58 case "logfmt": 59 return zaplogfmt.NewEncoder(config), nil 60 default: 61 return nil, fmt.Errorf("unknown logging format: %s", format) 62 } 63} 64 65func newEncoderConfig() zapcore.EncoderConfig { 66 config := zap.NewProductionEncoderConfig() 67 config.EncodeTime = func(ts time.Time, encoder zapcore.PrimitiveArrayEncoder) { 68 encoder.AppendString(ts.UTC().Format(TimeFormat)) 69 } 70 config.EncodeDuration = func(d time.Duration, encoder zapcore.PrimitiveArrayEncoder) { 71 val := float64(d) / float64(time.Millisecond) 72 encoder.AppendString(fmt.Sprintf("%.3fms", val)) 73 } 74 config.LevelKey = "lvl" 75 return config 76} 77 78// IsTerminal checks if w is a file and whether it is an interactive terminal session. 79func IsTerminal(w io.Writer) bool { 80 if f, ok := w.(interface { 81 Fd() uintptr 82 }); ok { 83 return isatty.IsTerminal(f.Fd()) 84 } 85 return false 86} 87 88const ( 89 year = 365 * 24 * time.Hour 90 week = 7 * 24 * time.Hour 91 day = 24 * time.Hour 92) 93 94func DurationLiteral(key string, val time.Duration) zapcore.Field { 95 if val == 0 { 96 return zap.String(key, "0s") 97 } 98 99 var ( 100 value int 101 unit string 102 ) 103 switch { 104 case val%year == 0: 105 value = int(val / year) 106 unit = "y" 107 case val%week == 0: 108 value = int(val / week) 109 unit = "w" 110 case val%day == 0: 111 value = int(val / day) 112 unit = "d" 113 case val%time.Hour == 0: 114 value = int(val / time.Hour) 115 unit = "h" 116 case val%time.Minute == 0: 117 value = int(val / time.Minute) 118 unit = "m" 119 case val%time.Second == 0: 120 value = int(val / time.Second) 121 unit = "s" 122 default: 123 value = int(val / time.Millisecond) 124 unit = "ms" 125 } 126 return zap.String(key, fmt.Sprintf("%d%s", value, unit)) 127} 128