1// Copyright (c) 2016 Uber Technologies, Inc.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in
11// all copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19// THE SOFTWARE.
20
21package zap
22
23import (
24	"fmt"
25	"sort"
26	"time"
27
28	"go.uber.org/zap/zapcore"
29)
30
31// SamplingConfig sets a sampling strategy for the logger. Sampling caps the
32// global CPU and I/O load that logging puts on your process while attempting
33// to preserve a representative subset of your logs.
34//
35// Values configured here are per-second. See zapcore.NewSampler for details.
36type SamplingConfig struct {
37	Initial    int `json:"initial" yaml:"initial"`
38	Thereafter int `json:"thereafter" yaml:"thereafter"`
39}
40
41// Config offers a declarative way to construct a logger. It doesn't do
42// anything that can't be done with New, Options, and the various
43// zapcore.WriteSyncer and zapcore.Core wrappers, but it's a simpler way to
44// toggle common options.
45//
46// Note that Config intentionally supports only the most common options. More
47// unusual logging setups (logging to network connections or message queues,
48// splitting output between multiple files, etc.) are possible, but require
49// direct use of the zapcore package. For sample code, see the package-level
50// BasicConfiguration and AdvancedConfiguration examples.
51//
52// For an example showing runtime log level changes, see the documentation for
53// AtomicLevel.
54type Config struct {
55	// Level is the minimum enabled logging level. Note that this is a dynamic
56	// level, so calling Config.Level.SetLevel will atomically change the log
57	// level of all loggers descended from this config.
58	Level AtomicLevel `json:"level" yaml:"level"`
59	// Development puts the logger in development mode, which changes the
60	// behavior of DPanicLevel and takes stacktraces more liberally.
61	Development bool `json:"development" yaml:"development"`
62	// DisableCaller stops annotating logs with the calling function's file
63	// name and line number. By default, all logs are annotated.
64	DisableCaller bool `json:"disableCaller" yaml:"disableCaller"`
65	// DisableStacktrace completely disables automatic stacktrace capturing. By
66	// default, stacktraces are captured for WarnLevel and above logs in
67	// development and ErrorLevel and above in production.
68	DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"`
69	// Sampling sets a sampling policy. A nil SamplingConfig disables sampling.
70	Sampling *SamplingConfig `json:"sampling" yaml:"sampling"`
71	// Encoding sets the logger's encoding. Valid values are "json" and
72	// "console", as well as any third-party encodings registered via
73	// RegisterEncoder.
74	Encoding string `json:"encoding" yaml:"encoding"`
75	// EncoderConfig sets options for the chosen encoder. See
76	// zapcore.EncoderConfig for details.
77	EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"`
78	// OutputPaths is a list of URLs or file paths to write logging output to.
79	// See Open for details.
80	OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`
81	// ErrorOutputPaths is a list of URLs to write internal logger errors to.
82	// The default is standard error.
83	//
84	// Note that this setting only affects internal errors; for sample code that
85	// sends error-level logs to a different location from info- and debug-level
86	// logs, see the package-level AdvancedConfiguration example.
87	ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"`
88	// InitialFields is a collection of fields to add to the root logger.
89	InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"`
90}
91
92// NewProductionEncoderConfig returns an opinionated EncoderConfig for
93// production environments.
94func NewProductionEncoderConfig() zapcore.EncoderConfig {
95	return zapcore.EncoderConfig{
96		TimeKey:        "ts",
97		LevelKey:       "level",
98		NameKey:        "logger",
99		CallerKey:      "caller",
100		MessageKey:     "msg",
101		StacktraceKey:  "stacktrace",
102		LineEnding:     zapcore.DefaultLineEnding,
103		EncodeLevel:    zapcore.LowercaseLevelEncoder,
104		EncodeTime:     zapcore.EpochTimeEncoder,
105		EncodeDuration: zapcore.SecondsDurationEncoder,
106		EncodeCaller:   zapcore.ShortCallerEncoder,
107	}
108}
109
110// NewProductionConfig is a reasonable production logging configuration.
111// Logging is enabled at InfoLevel and above.
112//
113// It uses a JSON encoder, writes to standard error, and enables sampling.
114// Stacktraces are automatically included on logs of ErrorLevel and above.
115func NewProductionConfig() Config {
116	return Config{
117		Level:       NewAtomicLevelAt(InfoLevel),
118		Development: false,
119		Sampling: &SamplingConfig{
120			Initial:    100,
121			Thereafter: 100,
122		},
123		Encoding:         "json",
124		EncoderConfig:    NewProductionEncoderConfig(),
125		OutputPaths:      []string{"stderr"},
126		ErrorOutputPaths: []string{"stderr"},
127	}
128}
129
130// NewDevelopmentEncoderConfig returns an opinionated EncoderConfig for
131// development environments.
132func NewDevelopmentEncoderConfig() zapcore.EncoderConfig {
133	return zapcore.EncoderConfig{
134		// Keys can be anything except the empty string.
135		TimeKey:        "T",
136		LevelKey:       "L",
137		NameKey:        "N",
138		CallerKey:      "C",
139		MessageKey:     "M",
140		StacktraceKey:  "S",
141		LineEnding:     zapcore.DefaultLineEnding,
142		EncodeLevel:    zapcore.CapitalLevelEncoder,
143		EncodeTime:     zapcore.ISO8601TimeEncoder,
144		EncodeDuration: zapcore.StringDurationEncoder,
145		EncodeCaller:   zapcore.ShortCallerEncoder,
146	}
147}
148
149// NewDevelopmentConfig is a reasonable development logging configuration.
150// Logging is enabled at DebugLevel and above.
151//
152// It enables development mode (which makes DPanicLevel logs panic), uses a
153// console encoder, writes to standard error, and disables sampling.
154// Stacktraces are automatically included on logs of WarnLevel and above.
155func NewDevelopmentConfig() Config {
156	return Config{
157		Level:            NewAtomicLevelAt(DebugLevel),
158		Development:      true,
159		Encoding:         "console",
160		EncoderConfig:    NewDevelopmentEncoderConfig(),
161		OutputPaths:      []string{"stderr"},
162		ErrorOutputPaths: []string{"stderr"},
163	}
164}
165
166// Build constructs a logger from the Config and Options.
167func (cfg Config) Build(opts ...Option) (*Logger, error) {
168	enc, err := cfg.buildEncoder()
169	if err != nil {
170		return nil, err
171	}
172
173	sink, errSink, err := cfg.openSinks()
174	if err != nil {
175		return nil, err
176	}
177
178	if cfg.Level == (AtomicLevel{}) {
179		return nil, fmt.Errorf("missing Level")
180	}
181
182	log := New(
183		zapcore.NewCore(enc, sink, cfg.Level),
184		cfg.buildOptions(errSink)...,
185	)
186	if len(opts) > 0 {
187		log = log.WithOptions(opts...)
188	}
189	return log, nil
190}
191
192func (cfg Config) buildOptions(errSink zapcore.WriteSyncer) []Option {
193	opts := []Option{ErrorOutput(errSink)}
194
195	if cfg.Development {
196		opts = append(opts, Development())
197	}
198
199	if !cfg.DisableCaller {
200		opts = append(opts, AddCaller())
201	}
202
203	stackLevel := ErrorLevel
204	if cfg.Development {
205		stackLevel = WarnLevel
206	}
207	if !cfg.DisableStacktrace {
208		opts = append(opts, AddStacktrace(stackLevel))
209	}
210
211	if cfg.Sampling != nil {
212		opts = append(opts, WrapCore(func(core zapcore.Core) zapcore.Core {
213			return zapcore.NewSampler(core, time.Second, int(cfg.Sampling.Initial), int(cfg.Sampling.Thereafter))
214		}))
215	}
216
217	if len(cfg.InitialFields) > 0 {
218		fs := make([]Field, 0, len(cfg.InitialFields))
219		keys := make([]string, 0, len(cfg.InitialFields))
220		for k := range cfg.InitialFields {
221			keys = append(keys, k)
222		}
223		sort.Strings(keys)
224		for _, k := range keys {
225			fs = append(fs, Any(k, cfg.InitialFields[k]))
226		}
227		opts = append(opts, Fields(fs...))
228	}
229
230	return opts
231}
232
233func (cfg Config) openSinks() (zapcore.WriteSyncer, zapcore.WriteSyncer, error) {
234	sink, closeOut, err := Open(cfg.OutputPaths...)
235	if err != nil {
236		return nil, nil, err
237	}
238	errSink, _, err := Open(cfg.ErrorOutputPaths...)
239	if err != nil {
240		closeOut()
241		return nil, nil, err
242	}
243	return sink, errSink, nil
244}
245
246func (cfg Config) buildEncoder() (zapcore.Encoder, error) {
247	return newEncoder(cfg.Encoding, cfg.EncoderConfig)
248}
249