/
zap.go
188 lines (157 loc) · 5.75 KB
/
zap.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
package zap
import (
"flag"
"fmt"
"io"
"reflect"
"strconv"
"strings"
"github.com/go-logr/logr"
"github.com/go-logr/zapr"
"github.com/mitchellh/mapstructure"
"github.com/spf13/viper"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
crzap "sigs.k8s.io/controller-runtime/pkg/log/zap"
)
// Options contains all possible settings.
type Options struct {
// Development configures the logger to use a Zap development config
// (stacktraces on warnings, no sampling), otherwise a Zap production
// config will be used (stacktraces on errors, sampling).
Development bool `mapstructure:"zap-devel"`
// Encoder configures how Zap will encode the output. Defaults to
// console when Development is true and JSON otherwise
Encoder zapcore.Encoder
// EncoderConfigOptions can modify the EncoderConfig needed to initialize an Encoder.
// See https://pkg.go.dev/go.uber.org/zap/zapcore#EncoderConfig for the list of options
// that can be configured.
// Note that the EncoderConfigOptions are not applied when the Encoder option is already set.
EncoderConfigOptions []crzap.EncoderConfigOption
// NewEncoder configures Encoder using the provided EncoderConfigOptions.
// Note that the NewEncoder function is not used when the Encoder option is already set.
NewEncoder crzap.NewEncoderFunc `mapstructure:"zap-encoder"`
// DestWriter controls the destination of the log output. Defaults to
// os.Stderr.
DestWriter io.Writer
// Level configures the verbosity of the logging.
// Defaults to Debug when Development is true and Info otherwise.
// A zap log level should be multiplied by -1 to get the logr verbosity.
// For example, to get logr verbosity of 3, set this field to zapcore.Level(-3).
// See https://pkg.go.dev/github.com/go-logr/zapr for how zap level relates to logr verbosity.
Level zapcore.LevelEnabler `mapstructure:"zap-log-level"`
// StacktraceLevel is the level at and above which stacktraces will
// be recorded for all messages. Defaults to Warn when Development
// is true and Error otherwise.
// See Level for the relationship of zap log level to logr verbosity.
StacktraceLevel zapcore.LevelEnabler `mapstructure:"zap-stacktrace-level"`
// ZapOpts allows passing arbitrary zap.Options to configure on the
// underlying Zap logger.
ZapOpts []zap.Option
// TimeEncoder specifies the encoder for the timestamps in log messages.
// Defaults to RFC3339TimeEncoder.
TimeEncoder zapcore.TimeEncoder `mapstructure:"zap-time-encoding"`
}
func UseFlagOptions(in *Options) crzap.Opts {
if err := viper.Unmarshal(in, viper.DecodeHook(
zapHook(),
)); err != nil {
panic(fmt.Errorf("unmarshal zap config: %w", err))
}
return func(o *crzap.Options) {
*o = crzap.Options(*in)
}
}
func (o *Options) BindFlags(fs *flag.FlagSet) {
zOpts := crzap.Options{}
zOpts.BindFlags(fs)
*o = Options(zOpts)
}
func New(opts ...crzap.Opts) logr.Logger {
return zapr.NewLogger(crzap.NewRaw(opts...))
}
var (
levelEnablerType = reflect.TypeOf((*zapcore.LevelEnabler)(nil)).Elem()
newEncoderFuncType = reflect.TypeOf((*crzap.NewEncoderFunc)(nil)).Elem()
)
/*
Following 2 encoder functions are copied from ControllerRuntime zap package.
We only set the EncoderFunc in the Hook function below and not initialize the Encoder.
This is done to ensure the TimeEncoder (passed in via env or the default) is used
while creating the Encoder in the ControllerRuntime code.
*/
func newConsoleEncoder(opts ...crzap.EncoderConfigOption) zapcore.Encoder {
encoderConfig := zap.NewDevelopmentEncoderConfig()
for _, opt := range opts {
opt(&encoderConfig)
}
return zapcore.NewConsoleEncoder(encoderConfig)
}
func newJSONEncoder(opts ...crzap.EncoderConfigOption) zapcore.Encoder {
encoderConfig := zap.NewProductionEncoderConfig()
for _, opt := range opts {
opt(&encoderConfig)
}
return zapcore.NewJSONEncoder(encoderConfig)
}
var levelStrings = map[string]zapcore.Level{
"debug": zap.DebugLevel,
"info": zap.InfoLevel,
"error": zap.ErrorLevel,
"panic": zap.PanicLevel,
}
func zapHook() mapstructure.DecodeHookFunc {
return mapstructure.ComposeDecodeHookFunc(
stringToLevelEnablerHookFunc(),
stringToNewEncoderFuncHookFunc(),
mapstructure.TextUnmarshallerHookFunc(),
)
}
func stringToLevelEnablerHookFunc() mapstructure.DecodeHookFuncType {
return func(in reflect.Type, out reflect.Type, val interface{}) (interface{}, error) {
if in.Kind() != reflect.String || out != levelEnablerType {
return val, nil
}
sVal := val.(string)
if sVal == "" {
var v zapcore.LevelEnabler
// return nil if level is not set; controller-runtime sets the default value
return &v, nil
}
// level supports setting of integer value > 0 in addition to `info`, `error` or `debug`
level, validLevel := levelStrings[strings.ToLower(sVal)]
if !validLevel {
logLevel, err := strconv.Atoi(sVal)
if err != nil {
return nil, fmt.Errorf("invalid log level \"%s\"", val)
}
if logLevel > 0 {
intLevel := -1 * logLevel
return zap.NewAtomicLevelAt(zapcore.Level(int8(intLevel))), nil
} else {
return nil, fmt.Errorf("invalid log level \"%s\"", val)
}
}
return zap.NewAtomicLevelAt(level), nil
}
}
func stringToNewEncoderFuncHookFunc() mapstructure.DecodeHookFuncType {
return func(in reflect.Type, out reflect.Type, val interface{}) (interface{}, error) {
if in.Kind() != reflect.String || out != newEncoderFuncType {
return val, nil
}
var encoder crzap.NewEncoderFunc
// TODO: implement encoding.TextUnmarshaler interface for type NewEncoderFunc upstream
switch val.(string) {
case "":
// return nil if encoder is not set; controller-runtime sets the default value
case "console":
encoder = newConsoleEncoder
case "json":
encoder = newJSONEncoder
default:
return nil, fmt.Errorf("invalid encoder value \"%s\"", val)
}
return encoder, nil
}
}