1// Copyright 2017 Istio Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package log
16
17import (
18	"fmt"
19	"sort"
20	"strings"
21
22	"github.com/spf13/cobra"
23)
24
25const (
26	DefaultScopeName          = "default"
27	OverrideScopeName         = "all"
28	defaultOutputLevel        = InfoLevel
29	defaultStackTraceLevel    = NoneLevel
30	defaultOutputPath         = "stdout"
31	defaultErrorOutputPath    = "stderr"
32	defaultRotationMaxAge     = 30
33	defaultRotationMaxSize    = 100 * 1024 * 1024
34	defaultRotationMaxBackups = 1000
35)
36
37// Level is an enumeration of all supported log levels.
38type Level int
39
40const (
41	// NoneLevel disables logging
42	NoneLevel Level = iota
43	// FatalLevel enables fatal level logging
44	FatalLevel
45	// ErrorLevel enables error level logging
46	ErrorLevel
47	// WarnLevel enables warn level logging
48	WarnLevel
49	// InfoLevel enables info level logging
50	InfoLevel
51	// DebugLevel enables debug level logging
52	DebugLevel
53)
54
55var levelToString = map[Level]string{
56	DebugLevel: "debug",
57	InfoLevel:  "info",
58	WarnLevel:  "warn",
59	ErrorLevel: "error",
60	FatalLevel: "fatal",
61	NoneLevel:  "none",
62}
63
64var stringToLevel = map[string]Level{
65	"debug": DebugLevel,
66	"info":  InfoLevel,
67	"warn":  WarnLevel,
68	"error": ErrorLevel,
69	"fatal": FatalLevel,
70	"none":  NoneLevel,
71}
72
73// Options defines the set of options supported by Istio's component logging package.
74type Options struct {
75	// OutputPaths is a list of file system paths to write the log data to.
76	// The special values stdout and stderr can be used to output to the
77	// standard I/O streams. This defaults to stdout.
78	OutputPaths []string
79
80	// ErrorOutputPaths is a list of file system paths to write logger errors to.
81	// The special values stdout and stderr can be used to output to the
82	// standard I/O streams. This defaults to stderr.
83	ErrorOutputPaths []string
84
85	// RotateOutputPath is the path to a rotating log file. This file should
86	// be automatically rotated over time, based on the rotation parameters such
87	// as RotationMaxSize and RotationMaxAge. The default is to not rotate.
88	//
89	// This path is used as a foundational path. This is where log output is normally
90	// saved. When a rotation needs to take place because the file got too big or too
91	// old, then the file is renamed by appending a timestamp to the name. Such renamed
92	// files are called backups. Once a backup has been created,
93	// output resumes to this path.
94	RotateOutputPath string
95
96	// RotationMaxSize is the maximum size in megabytes of a log file before it gets
97	// rotated. It defaults to 100 megabytes.
98	RotationMaxSize int
99
100	// RotationMaxAge is the maximum number of days to retain old log files based on the
101	// timestamp encoded in their filename. Note that a day is defined as 24
102	// hours and may not exactly correspond to calendar days due to daylight
103	// savings, leap seconds, etc. The default is to remove log files
104	// older than 30 days.
105	RotationMaxAge int
106
107	// RotationMaxBackups is the maximum number of old log files to retain.  The default
108	// is to retain at most 1000 logs.
109	RotationMaxBackups int
110
111	// JSONEncoding controls whether the log is formatted as JSON.
112	JSONEncoding bool
113
114	// LogGrpc indicates that Grpc logs should be captured. The default is true.
115	// This is not exposed through the command-line flags, as this flag is mainly useful for testing: Grpc
116	// stack will hold on to the logger even though it gets closed. This causes data races.
117	LogGrpc bool
118
119	outputLevels     string
120	logCallers       string
121	stackTraceLevels string
122}
123
124// DefaultOptions returns a new set of options, initialized to the defaults
125func DefaultOptions() *Options {
126	return &Options{
127		OutputPaths:        []string{defaultOutputPath},
128		ErrorOutputPaths:   []string{defaultErrorOutputPath},
129		RotationMaxSize:    defaultRotationMaxSize,
130		RotationMaxAge:     defaultRotationMaxAge,
131		RotationMaxBackups: defaultRotationMaxBackups,
132		outputLevels:       DefaultScopeName + ":" + levelToString[defaultOutputLevel],
133		stackTraceLevels:   DefaultScopeName + ":" + levelToString[defaultStackTraceLevel],
134		LogGrpc:            true,
135	}
136}
137
138// SetOutputLevel sets the minimum log output level for a given scope.
139func (o *Options) SetOutputLevel(scope string, level Level) {
140	sl := scope + ":" + levelToString[level]
141	levels := strings.Split(o.outputLevels, ",")
142
143	if scope == DefaultScopeName {
144		// see if we have an entry without a scope prefix (which represents the default scope)
145		for i, ol := range levels {
146			if !strings.Contains(ol, ":") {
147				levels[i] = sl
148				o.outputLevels = strings.Join(levels, ",")
149				return
150			}
151		}
152	}
153
154	prefix := scope + ":"
155	for i, ol := range levels {
156		if strings.HasPrefix(ol, prefix) {
157			levels[i] = sl
158			o.outputLevels = strings.Join(levels, ",")
159			return
160		}
161	}
162
163	levels = append(levels, sl)
164	o.outputLevels = strings.Join(levels, ",")
165}
166
167// GetOutputLevel returns the minimum log output level for a given scope.
168func (o *Options) GetOutputLevel(scope string) (Level, error) {
169	levels := strings.Split(o.outputLevels, ",")
170
171	if scope == DefaultScopeName {
172		// see if we have an entry without a scope prefix (which represents the default scope)
173		for _, ol := range levels {
174			if !strings.Contains(ol, ":") {
175				_, l, err := convertScopedLevel(ol)
176				return l, err
177			}
178		}
179	}
180
181	prefix := scope + ":"
182	for _, ol := range levels {
183		if strings.HasPrefix(ol, prefix) {
184			_, l, err := convertScopedLevel(ol)
185			return l, err
186		}
187	}
188
189	return NoneLevel, fmt.Errorf("no level defined for scope '%s'", scope)
190}
191
192// SetStackTraceLevel sets the minimum stack tracing level for a given scope.
193func (o *Options) SetStackTraceLevel(scope string, level Level) {
194	sl := scope + ":" + levelToString[level]
195	levels := strings.Split(o.stackTraceLevels, ",")
196
197	if scope == DefaultScopeName {
198		// see if we have an entry without a scope prefix (which represents the default scope)
199		for i, ol := range levels {
200			if !strings.Contains(ol, ":") {
201				levels[i] = sl
202				o.stackTraceLevels = strings.Join(levels, ",")
203				return
204			}
205		}
206	}
207
208	prefix := scope + ":"
209	for i, ol := range levels {
210		if strings.HasPrefix(ol, prefix) {
211			levels[i] = sl
212			o.stackTraceLevels = strings.Join(levels, ",")
213			return
214		}
215	}
216
217	levels = append(levels, sl)
218	o.stackTraceLevels = strings.Join(levels, ",")
219}
220
221// GetStackTraceLevel returns the minimum stack tracing level for a given scope.
222func (o *Options) GetStackTraceLevel(scope string) (Level, error) {
223	levels := strings.Split(o.stackTraceLevels, ",")
224
225	if scope == DefaultScopeName {
226		// see if we have an entry without a scope prefix (which represents the default scope)
227		for _, ol := range levels {
228			if !strings.Contains(ol, ":") {
229				_, l, err := convertScopedLevel(ol)
230				return l, err
231			}
232		}
233	}
234
235	prefix := scope + ":"
236	for _, ol := range levels {
237		if strings.HasPrefix(ol, prefix) {
238			_, l, err := convertScopedLevel(ol)
239			return l, err
240		}
241	}
242
243	return NoneLevel, fmt.Errorf("no level defined for scope '%s'", scope)
244}
245
246// SetLogCallers sets whether to output the caller's source code location for a given scope.
247func (o *Options) SetLogCallers(scope string, include bool) {
248	scopes := strings.Split(o.logCallers, ",")
249
250	// remove any occurrence of the scope
251	for i, s := range scopes {
252		if s == scope {
253			scopes[i] = ""
254		}
255	}
256
257	if include {
258		// find a free slot if there is one
259		for i, s := range scopes {
260			if s == "" {
261				scopes[i] = scope
262				o.logCallers = strings.Join(scopes, ",")
263				return
264			}
265		}
266
267		scopes = append(scopes, scope)
268	}
269
270	o.logCallers = strings.Join(scopes, ",")
271}
272
273// GetLogCallers returns whether the caller's source code location is output for a given scope.
274func (o *Options) GetLogCallers(scope string) bool {
275	scopes := strings.Split(o.logCallers, ",")
276
277	for _, s := range scopes {
278		if s == scope {
279			return true
280		}
281	}
282
283	return false
284}
285
286func convertScopedLevel(sl string) (string, Level, error) {
287	var s string
288	var l string
289
290	pieces := strings.Split(sl, ":")
291	if len(pieces) == 1 {
292		s = DefaultScopeName
293		l = pieces[0]
294	} else if len(pieces) == 2 {
295		s = pieces[0]
296		l = pieces[1]
297	} else {
298		return "", NoneLevel, fmt.Errorf("invalid output level format '%s'", sl)
299	}
300
301	level, ok := stringToLevel[l]
302	if !ok {
303		return "", NoneLevel, fmt.Errorf("invalid output level '%s'", sl)
304	}
305
306	return s, level, nil
307}
308
309// AttachCobraFlags attaches a set of Cobra flags to the given Cobra command.
310//
311// Cobra is the command-line processor that Istio uses. This command attaches
312// the necessary set of flags to expose a CLI to let the user control all
313// logging options.
314func (o *Options) AttachCobraFlags(cmd *cobra.Command) {
315	o.AttachFlags(
316		cmd.PersistentFlags().StringArrayVar,
317		cmd.PersistentFlags().StringVar,
318		cmd.PersistentFlags().IntVar,
319		cmd.PersistentFlags().BoolVar)
320}
321
322// AttachFlags allows attaching of flags through a set of lambda functions.
323func (o *Options) AttachFlags(
324	stringArrayVar func(p *[]string, name string, value []string, usage string),
325	stringVar func(p *string, name string, value string, usage string),
326	intVar func(p *int, name string, value int, usage string),
327	boolVar func(p *bool, name string, value bool, usage string)) {
328
329	stringArrayVar(&o.OutputPaths, "log_target", o.OutputPaths,
330		"The set of paths where to output the log. This can be any path as well as the special values stdout and stderr")
331
332	stringVar(&o.RotateOutputPath, "log_rotate", o.RotateOutputPath,
333		"The path for the optional rotating log file")
334
335	intVar(&o.RotationMaxAge, "log_rotate_max_age", o.RotationMaxAge,
336		"The maximum age in days of a log file beyond which the file is rotated (0 indicates no limit)")
337
338	intVar(&o.RotationMaxSize, "log_rotate_max_size", o.RotationMaxSize,
339		"The maximum size in megabytes of a log file beyond which the file is rotated")
340
341	intVar(&o.RotationMaxBackups, "log_rotate_max_backups", o.RotationMaxBackups,
342		"The maximum number of log file backups to keep before older files are deleted (0 indicates no limit)")
343
344	boolVar(&o.JSONEncoding, "log_as_json", o.JSONEncoding,
345		"Whether to format output as JSON or in plain console-friendly format")
346
347	levelListString := fmt.Sprintf("[%s, %s, %s, %s, %s, %s]",
348		levelToString[DebugLevel],
349		levelToString[InfoLevel],
350		levelToString[WarnLevel],
351		levelToString[ErrorLevel],
352		levelToString[FatalLevel],
353		levelToString[NoneLevel])
354
355	allScopes := Scopes()
356	if len(allScopes) > 1 {
357		keys := make([]string, 0, len(allScopes))
358		for name := range allScopes {
359			keys = append(keys, name)
360		}
361		keys = append(keys, OverrideScopeName)
362		sort.Strings(keys)
363		s := strings.Join(keys, ", ")
364
365		stringVar(&o.outputLevels, "log_output_level", o.outputLevels,
366			fmt.Sprintf("Comma-separated minimum per-scope logging level of messages to output, in the form of "+
367				"<scope>:<level>,<scope>:<level>,... where scope can be one of [%s] and level can be one of %s",
368				s, levelListString))
369
370		stringVar(&o.stackTraceLevels, "log_stacktrace_level", o.stackTraceLevels,
371			fmt.Sprintf("Comma-separated minimum per-scope logging level at which stack traces are captured, in the form of "+
372				"<scope>:<level>,<scope:level>,... where scope can be one of [%s] and level can be one of %s",
373				s, levelListString))
374
375		stringVar(&o.logCallers, "log_caller", o.logCallers,
376			fmt.Sprintf("Comma-separated list of scopes for which to include caller information, scopes can be any of [%s]", s))
377	} else {
378		stringVar(&o.outputLevels, "log_output_level", o.outputLevels,
379			fmt.Sprintf("The minimum logging level of messages to output,  can be one of %s",
380				levelListString))
381
382		stringVar(&o.stackTraceLevels, "log_stacktrace_level", o.stackTraceLevels,
383			fmt.Sprintf("The minimum logging level at which stack traces are captured, can be one of %s",
384				levelListString))
385
386		stringVar(&o.logCallers, "log_caller", o.logCallers,
387			"Comma-separated list of scopes for which to include called information, scopes can be any of [default]")
388	}
389
390	// NOTE: we don't currently expose a command-line option to control ErrorOutputPaths since it
391	// seems too esoteric.
392}
393