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