server: add support for log rotation

Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
release-3.5
Sam Batschelet 2021-05-05 13:29:22 -04:00
parent eb128d2d8c
commit 82b2d5c67d
11 changed files with 184 additions and 9 deletions

View File

@ -638,6 +638,15 @@
} }
] ]
}, },
{
"project": "gopkg.in/natefinch/lumberjack.v2",
"licenses": [
{
"type": "MIT License",
"confidence": 1
}
]
},
{ {
"project": "gopkg.in/yaml.v2", "project": "gopkg.in/yaml.v2",
"licenses": [ "licenses": [

View File

@ -469,6 +469,7 @@ gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk=
gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

3
go.sum
View File

@ -11,6 +11,7 @@ cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqCl
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
@ -477,6 +478,8 @@ gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk=
gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -68,6 +68,15 @@ const (
StdErrLogOutput = "stderr" StdErrLogOutput = "stderr"
StdOutLogOutput = "stdout" StdOutLogOutput = "stdout"
// DefaultLogRotationConfig is the default configuration used for log rotation.
// Log rotation is disabled by default.
// MaxSize = 100 // MB
// MaxAge = 0 // days (no limit)
// MaxBackups = 0 // no limit
// LocalTime = false // use computers local time, UTC by default
// Compress = false // compress the rotated log in gzip format
DefaultLogRotationConfig = `{"maxsize": 100, "maxage": 0, "maxbackups": 0, "localtime": false, "compress": false}`
// DefaultStrictReconfigCheck is the default value for "--strict-reconfig-check" flag. // DefaultStrictReconfigCheck is the default value for "--strict-reconfig-check" flag.
// It's enabled by default. // It's enabled by default.
DefaultStrictReconfigCheck = true DefaultStrictReconfigCheck = true
@ -86,6 +95,7 @@ var (
ErrConflictBootstrapFlags = fmt.Errorf("multiple discovery or bootstrap flags are set. " + ErrConflictBootstrapFlags = fmt.Errorf("multiple discovery or bootstrap flags are set. " +
"Choose one of \"initial-cluster\", \"discovery\" or \"discovery-srv\"") "Choose one of \"initial-cluster\", \"discovery\" or \"discovery-srv\"")
ErrUnsetAdvertiseClientURLsFlag = fmt.Errorf("--advertise-client-urls is required when --listen-client-urls is set explicitly") ErrUnsetAdvertiseClientURLsFlag = fmt.Errorf("--advertise-client-urls is required when --listen-client-urls is set explicitly")
ErrLogRotationInvalidLogOutput = fmt.Errorf("--log-outputs requires a single file path when --log-rotate-config-json is defined")
DefaultInitialAdvertisePeerURLs = "http://localhost:2380" DefaultInitialAdvertisePeerURLs = "http://localhost:2380"
DefaultAdvertiseClientURLs = "http://localhost:2379" DefaultAdvertiseClientURLs = "http://localhost:2379"
@ -320,7 +330,10 @@ type Config struct {
// - file path to append server logs to. // - file path to append server logs to.
// It can be multiple when "Logger" is zap. // It can be multiple when "Logger" is zap.
LogOutputs []string `json:"log-outputs"` LogOutputs []string `json:"log-outputs"`
// EnableLogRotation enables log rotation of a single LogOutputs file target.
EnableLogRotation bool `json:"enable-log-rotation"`
// LogRotationConfigJSON is a passthrough allowing a log rotation JSON config to be passed directly.
LogRotationConfigJSON string `json:"log-rotation-config-json"`
// ZapLoggerBuilder is used to build the zap logger. // ZapLoggerBuilder is used to build the zap logger.
ZapLoggerBuilder func(*Config) error ZapLoggerBuilder func(*Config) error
@ -437,12 +450,14 @@ func NewConfig() *Config {
PreVote: true, PreVote: true,
loggerMu: new(sync.RWMutex), loggerMu: new(sync.RWMutex),
logger: nil, logger: nil,
Logger: "zap", Logger: "zap",
LogOutputs: []string{DefaultLogOutput}, LogOutputs: []string{DefaultLogOutput},
LogLevel: logutil.DefaultLogLevel, LogLevel: logutil.DefaultLogLevel,
EnableGRPCGateway: true, EnableLogRotation: false,
LogRotationConfigJSON: DefaultLogRotationConfig,
EnableGRPCGateway: true,
ExperimentalDowngradeCheckTime: DefaultDowngradeCheckTime, ExperimentalDowngradeCheckTime: DefaultDowngradeCheckTime,
ExperimentalMemoryMlock: false, ExperimentalMemoryMlock: false,

View File

@ -16,8 +16,11 @@ package embed
import ( import (
"crypto/tls" "crypto/tls"
"encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/url"
"os" "os"
"go.etcd.io/etcd/client/pkg/v3/logutil" "go.etcd.io/etcd/client/pkg/v3/logutil"
@ -26,6 +29,7 @@ import (
"go.uber.org/zap/zapgrpc" "go.uber.org/zap/zapgrpc"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/grpclog" "google.golang.org/grpc/grpclog"
"gopkg.in/natefinch/lumberjack.v2"
) )
// GetLogger returns the logger. // GetLogger returns the logger.
@ -54,6 +58,11 @@ func (cfg *Config) setupLogging() error {
} }
} }
} }
if cfg.EnableLogRotation {
if err := setupLogRotation(cfg.LogOutputs, cfg.LogRotationConfigJSON); err != nil {
return err
}
}
outputPaths, errOutputPaths := make([]string, 0), make([]string, 0) outputPaths, errOutputPaths := make([]string, 0), make([]string, 0)
isJournal := false isJournal := false
@ -75,8 +84,15 @@ func (cfg *Config) setupLogging() error {
errOutputPaths = append(errOutputPaths, StdOutLogOutput) errOutputPaths = append(errOutputPaths, StdOutLogOutput)
default: default:
outputPaths = append(outputPaths, v) var path string
errOutputPaths = append(errOutputPaths, v) if cfg.EnableLogRotation {
// append rotate scheme to logs managed by lumberjack log rotation
path = fmt.Sprintf("rotate:%s", v)
} else {
path = v
}
outputPaths = append(outputPaths, path)
errOutputPaths = append(errOutputPaths, path)
} }
} }
@ -211,3 +227,48 @@ func (cfg *Config) SetupGlobalLoggers() {
zap.ReplaceGlobals(lg) zap.ReplaceGlobals(lg)
} }
} }
type logRotationConfig struct {
*lumberjack.Logger
}
// Sync implements zap.Sink
func (logRotationConfig) Sync() error { return nil }
// setupLogRotation initializes log rotation for a single file path target.
func setupLogRotation(logOutputs []string, logRotateConfigJSON string) error {
var logRotationConfig logRotationConfig
outputFilePaths := 0
for _, v := range logOutputs {
switch v {
case DefaultLogOutput, StdErrLogOutput, StdOutLogOutput:
continue
default:
outputFilePaths++
}
}
// log rotation requires file target
if len(logOutputs) == 1 && outputFilePaths == 0 {
return ErrLogRotationInvalidLogOutput
}
// support max 1 file target for log rotation
if outputFilePaths > 1 {
return ErrLogRotationInvalidLogOutput
}
if err := json.Unmarshal([]byte(logRotateConfigJSON), &logRotationConfig); err != nil {
var unmarshalTypeError *json.UnmarshalTypeError
var syntaxError *json.SyntaxError
switch {
case errors.As(err, &syntaxError):
return fmt.Errorf("improperly formatted log rotation config: %w", err)
case errors.As(err, &unmarshalTypeError):
return fmt.Errorf("invalid log rotation config: %w", err)
}
}
zap.RegisterSink("rotate", func(u *url.URL) (zap.Sink, error) {
logRotationConfig.Filename = u.Path
return &logRotationConfig, nil
})
return nil
}

View File

@ -15,6 +15,7 @@
package embed package embed
import ( import (
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net" "net"
@ -289,3 +290,77 @@ func TestPeerURLsMapAndTokenFromSRV(t *testing.T) {
} }
} }
} }
func TestLogRotation(t *testing.T) {
tests := []struct {
name string
logOutputs []string
logRotationConfig string
wantErr bool
wantErrMsg error
}{
{
name: "mixed log output targets",
logOutputs: []string{"stderr", "/tmp/path"},
logRotationConfig: `{"maxsize": 1}`,
},
{
name: "no file targets",
logOutputs: []string{"stderr"},
logRotationConfig: `{"maxsize": 1}`,
wantErr: true,
wantErrMsg: ErrLogRotationInvalidLogOutput,
},
{
name: "multiple file targets",
logOutputs: []string{"/tmp/path1", "/tmp/path2"},
logRotationConfig: DefaultLogRotationConfig,
wantErr: true,
wantErrMsg: ErrLogRotationInvalidLogOutput,
},
{
name: "default output",
logRotationConfig: `{"maxsize": 1}`,
wantErr: true,
wantErrMsg: ErrLogRotationInvalidLogOutput,
},
{
name: "default log rotation config",
logOutputs: []string{"/tmp/path"},
logRotationConfig: DefaultLogRotationConfig,
},
{
name: "invalid logger config",
logOutputs: []string{"/tmp/path"},
logRotationConfig: `{"maxsize": true}`,
wantErr: true,
wantErrMsg: errors.New("invalid log rotation config: json: cannot unmarshal bool into Go struct field logRotationConfig.maxsize of type int"),
},
{
name: "improperly formatted logger config",
logOutputs: []string{"/tmp/path"},
logRotationConfig: `{"maxsize": true`,
wantErr: true,
wantErrMsg: errors.New("improperly formatted log rotation config: unexpected end of JSON input"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := NewConfig()
cfg.Logger = "zap"
cfg.LogOutputs = tt.logOutputs
cfg.EnableLogRotation = true
cfg.LogRotationConfigJSON = tt.logRotationConfig
err := cfg.Validate()
if err != nil && !tt.wantErr {
t.Errorf("test %q, unexpected error %v", tt.name, err)
}
if err != nil && tt.wantErr && tt.wantErrMsg.Error() != err.Error() {
t.Errorf("test %q, expected error: %+v, got: %+v", tt.name, tt.wantErrMsg, err)
}
if err == nil && tt.wantErr {
t.Errorf("test %q, expected error, got nil", tt.name)
}
})
}
}

View File

@ -236,6 +236,8 @@ func newConfig() *config {
fs.StringVar(&cfg.ec.Logger, "logger", "zap", "Currently only supports 'zap' for structured logging.") fs.StringVar(&cfg.ec.Logger, "logger", "zap", "Currently only supports 'zap' for structured logging.")
fs.Var(flags.NewUniqueStringsValue(embed.DefaultLogOutput), "log-outputs", "Specify 'stdout' or 'stderr' to skip journald logging even when running under systemd, or list of comma separated output targets.") fs.Var(flags.NewUniqueStringsValue(embed.DefaultLogOutput), "log-outputs", "Specify 'stdout' or 'stderr' to skip journald logging even when running under systemd, or list of comma separated output targets.")
fs.StringVar(&cfg.ec.LogLevel, "log-level", logutil.DefaultLogLevel, "Configures log level. Only supports debug, info, warn, error, panic, or fatal. Default 'info'.") fs.StringVar(&cfg.ec.LogLevel, "log-level", logutil.DefaultLogLevel, "Configures log level. Only supports debug, info, warn, error, panic, or fatal. Default 'info'.")
fs.BoolVar(&cfg.ec.EnableLogRotation, "enable-log-rotation", false, "Enable log rotation of a single log-outputs file target.")
fs.StringVar(&cfg.ec.LogRotationConfigJSON, "log-rotation-config-json", embed.DefaultLogRotationConfig, "Configures log rotation if enabled with a JSON logger config. Default: MaxSize=100(MB), MaxAge=0(days,no limit), MaxBackups=0(no limit), LocalTime=false(UTC), Compress=false(gzip)")
// version // version
fs.BoolVar(&cfg.printVersion, "version", false, "Print the version and exit.") fs.BoolVar(&cfg.printVersion, "version", false, "Print the version and exit.")

View File

@ -188,6 +188,10 @@ Logging:
Specify 'stdout' or 'stderr' to skip journald logging even when running under systemd, or list of comma separated output targets. Specify 'stdout' or 'stderr' to skip journald logging even when running under systemd, or list of comma separated output targets.
--log-level 'info' --log-level 'info'
Configures log level. Only supports debug, info, warn, error, panic, or fatal. Configures log level. Only supports debug, info, warn, error, panic, or fatal.
--enable-log-rotation 'false'
Enable log rotation of a single log-outputs file target.
--log-rotation-config-json '{"maxsize": 100, "maxage": 0, "maxbackups": 0, "localtime": false, "compress": false}'
Configures log rotation if enabled with a JSON logger config. MaxSize(MB), MaxAge(days,0=no limit), MaxBackups(0=no limit), LocalTime(use computers local time), Compress(gzip)".
v2 Proxy (to be deprecated in v4): v2 Proxy (to be deprecated in v4):
--proxy 'off' --proxy 'off'

View File

@ -40,6 +40,7 @@ require (
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013
google.golang.org/grpc v1.36.1 google.golang.org/grpc v1.36.1
gopkg.in/natefinch/lumberjack.v2 v2.0.0
sigs.k8s.io/yaml v1.2.0 sigs.k8s.io/yaml v1.2.0
) )

View File

@ -463,6 +463,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -472,6 +472,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=