-
Notifications
You must be signed in to change notification settings - Fork 0
/
config.go
338 lines (278 loc) · 8.98 KB
/
config.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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
package log
import (
"fmt"
"io"
"net/url"
"os"
"reflect"
"strings"
)
//EnvKey is a publicly documented string type for environment lookups performed for DefaultConfig.
//It is otherwise unspecial.
type EnvKey string
const (
//LogLevel is the EnvKey used for looking up the log level.
LogLevel EnvKey = "LOG_LEVEL"
//ErrStack is the EnvKey used for looking up error/stack trace logging; Value should be true || True || TRUE.
ErrStack EnvKey = "ERROR_STACK"
//SentryDSN is the EnvKey used for looking up the Sentry project DNS; Empty value disables Sentry.
SentryDSN EnvKey = "SENTRY_DSN"
//SentryLevels is the EnvKey used for looking up which log levels will be sent to Sentry; Values should be comma separated.
SentryLevels EnvKey = "SENTRY_LEVELS"
//Environment is the EnvKey used for looking up the current deployment environment; Values commonly dev || prod.
Environment EnvKey = "ENVIRONMENT"
)
func (ek EnvKey) String() string {
return string(ek)
}
//Config defines common logger configuration options.
type Config struct {
//Level is the level at which returned Logger's will be considered enabled.
//For example, setting WARN then logging and sending a Debug entry will cause
//the entry to not be logged.
Level Level
//LocalDevel, may be used by some Logger's for local debugging changes.
LocalDevel bool
//Format is the format the Logger should log in.
Format LoggerFormat
//EnableErrStack enables error stack gathering and logging.
EnableErrStack bool
//Output is the io.Writer the Logger will write messages to.
Output io.Writer
//Sentry is a sub-config type for configurating Sentry if desired.
//No other portion of this struct is considered if DSN is not set and valid.
Sentry struct {
//DSN is the Sentry DSN.
DSN string
//Release is the program release or revision.
Release string
//Env is the deployment environment; "prod", "dev", etc.
Env string
//Server is the server or hostname.
Server string
//Levels are the log levels that will trigger an event to be sent to Sentry.
Levels []Level
//Debug is a passthrough for Sentry debugging.
Debug bool
}
}
//DefaultConfig returns a Config instance with sane defaults.
//env is a callback for looking up EnvKeys, it is set to os.Getenv if nil.
//Fields and values returned by this function can be altered.
func DefaultConfig(env func(string) string) *Config {
/*
TODO: Configuration is still an issue, as discussed during design phase.
If we use consol + vault, most of our values should become env vars.
This is a work in progress.
*/
conf := new(Config)
if env == nil {
env = os.Getenv
}
if lvlStr := env(LogLevel.String()); lvlStr != "" {
conf.Level = LevelFromString(lvlStr)
}
if errStackStr := env(ErrStack.String()); errStackStr != "" {
conf.EnableErrStack = strings.ToUpper(errStackStr) == "TRUE"
}
sentryDSN := env(SentryDSN.String())
if sentryDSN != "" {
lvls := []Level{FATAL, PANIC, ERROR}
split := strings.Split(env(SentryLevels.String()), ",")
if len(split) > 0 && split[0] != "" {
lvlSet := make(map[Level]bool, len(split))
for _, lvl := range split {
lvlSet[LevelFromString(lvl)] = true
}
lvls = make([]Level, 0, len(lvlSet))
for lvl := range lvlSet {
lvls = append(lvls, lvl)
}
}
host, _ := os.Hostname()
conf.Sentry.DSN = sentryDSN
conf.Sentry.Levels = lvls
// TODO: fix this
//conf.Sentry.Release = version.Get().Revision
conf.Sentry.Server = host
conf.Sentry.Env = env(Environment.String())
}
if _, err := url.Parse(sentryDSN); err != nil {
conf.Sentry.DSN = ""
}
conf.Output = os.Stderr
return conf
}
//NewLogger is a function type for Logger implemenations to register themselves.
type NewLogger func(*Config, ...Option) (Logger, error)
var (
setup = make(map[string]NewLogger, 2)
)
//Register registers the provided NewLogger function under the given name for use with Open.
//Note, this method is not concurreny safe, nil NewLoggers or duplicate registration
//will cause a panic.
func Register(name string, nl NewLogger) {
if _, ok := setup[name]; ok || nl == nil {
panic(fmt.Errorf("log: %s already registered with logging package", name))
}
setup[name] = nl
}
//Open returns a new instance of the selected Logger with config and options.
func Open(name string, conf *Config, opts ...Option) (Logger, error) {
nl, ok := setup[name]
if !ok {
return nil, fmt.Errorf("log: No logger by name (%s)", name)
}
if conf == nil {
conf = DefaultConfig(nil)
}
return nl(conf, opts...)
}
//Option is a function type that accepts an interface value and returns an error.
type Option func(interface{}) error
func noopOption(_ interface{}) error { return nil }
func errOption(err error) Option {
return func(_ interface{}) error {
return err
}
}
//CustomOption takes the name of a method on the UnderlyingLogger of a Logger implementation
//as well as a value, and returns an Option. name is the case sensitive name of a method while
//val should be a single value needed as input to the named method. If several values are needed as input
//then val should be a function that accepts no input and returns values to be used as input to the
//named method. If val is a function and returns an error as its only or last value it will be checked
//and returned if non nil, otherwise remaining values are fed to the named method.
//A nil val is valid so long as the named method expects nil input or no input.
//If the named method returns an instance of itself it will be set back as the new UnderlyingLogger.
//If the named method returns an error that error will be checked and returned.
//Look to the CustomOption tests and package level example for basic usage.
func CustomOption(name string, val interface{}) Option {
if name == "" {
return noopOption
}
valFunc, err := getReflectVals(val)
if err != nil {
return errOption(err)
}
return func(topLogger interface{}) (err error) {
ul, ok := topLogger.(UnderlyingLogger)
if !ok {
return fmt.Errorf("log: Logger type (%T) does not support the UnderlyingLogger interface", topLogger)
}
defer func() {
pv := recover()
if pv == nil || err != nil {
return
}
if e, ok := pv.(error); ok {
err = e
} else {
err = fmt.Errorf("log: Panic caught in CustomOption for %s: %v", name, pv)
}
}()
//get logger and check if nil, if it is this was
//intentional by implementation and we should just return
logger := ul.GetLogger()
if logger == nil {
return
}
logVal := reflect.ValueOf(logger)
if !logVal.IsValid() {
return
}
methVal := logVal.MethodByName(name)
if !methVal.IsValid() {
return
}
var wasError bool
vals := valFunc()
//check if last val is error or error that is nil
if l := len(vals); l > 0 {
err, wasError = valueToError(vals[l-1])
if err != nil {
return
}
//remove value
if wasError {
vals = vals[:l-1]
}
}
//from this point we have the method we want to call and
//the values with which to call it
//we could check if each input matches what is expected but
//instead we'll just call the method and rely on the defer recover
//above to stop us from calling something wrong
out := methVal.Call(vals)
le := len(out)
if le == 0 {
return
}
err, wasError = valueToError(out[le-1])
if err != nil {
return
}
if wasError {
out = out[:le-1]
le = len(out)
}
if le == 0 {
return
}
//if one of the remaining types in the output is the same
//as the underlying logger, set it back as the underlying logger
//this is common in methods/funcs that chain configuration
logType := logVal.Type()
for _, val := range out {
if !val.IsValid() {
continue
}
/*
Since logType is an interface this never seems correct, but according to
https://golang.org/pkg/reflect/#Type
"Type values are comparable, such as with the == operator, so they can be used as map keys.
Two Type values are equal if they represent identical types."
*/
if val.Type() == logType {
ul.SetLogger(val.Interface())
break
}
}
return
}
}
func getReflectVals(val interface{}) (func() []reflect.Value, error) {
reflval := reflect.ValueOf(val)
if !reflval.IsValid() {
return func() []reflect.Value { return []reflect.Value{} }, nil
}
typ := reflval.Type()
if typ.Kind() != reflect.Func {
return func() []reflect.Value { return []reflect.Value{reflval} }, nil
}
//we know it's a func, check to make sure it doesn't take args
if typ.NumIn() > 0 {
name := typ.Name()
if name == "" {
name = "anon func"
}
return nil, fmt.Errorf("log: Function value (%s) expects inputs", name)
}
return func() []reflect.Value { return reflval.Call([]reflect.Value{}) }, nil
}
var errInterface = reflect.TypeOf((*error)(nil)).Elem()
//checks and converts a reflect.Value to an error if appropriate.
//nolint
func valueToError(val reflect.Value) (err error, wasError bool) {
if !val.IsValid() {
return
}
if val.Kind() != reflect.Interface || !val.Type().Implements(errInterface) {
return
}
wasError = true
if val.IsNil() {
return
}
err = val.Elem().Interface().(error)
return
}