From: wyjDoraemon <46176410+wyjDoraemon@users.noreply.github.com> Date: Mon, 29 Jul 2019 02:21:04 +0000 (+0800) Subject: log into file (#357) X-Git-Tag: v1.0.5~70 X-Git-Url: http://git.osdn.net/view?p=bytom%2Fvapor.git;a=commitdiff_plain;h=bc213b29d91743bb9cb23c043f2856f47b34bb3e log into file (#357) * log into file * fig log * fix * fix log into file * ignore * vendor lestrrat * fix grammar * final * fix * fix fix * fix --- diff --git a/config/config.go b/config/config.go index 8b5ede84..02d2c7cf 100644 --- a/config/config.go +++ b/config/config.go @@ -134,6 +134,7 @@ func DefaultBaseConfig() BaseConfig { DBBackend: "leveldb", DBPath: "data", KeysPath: "keystore", + LogFile: "log", PrivateKeyFile: "node_key.txt", FederationFileName: "federation.json", } @@ -143,6 +144,10 @@ func (b BaseConfig) DBDir() string { return rootify(b.DBPath, b.RootDir) } +func (b BaseConfig) LogDir() string { + return rootify(b.LogFile, b.RootDir) +} + func (b BaseConfig) KeysDir() string { return rootify(b.KeysPath, b.RootDir) } diff --git a/log/log.go b/log/log.go new file mode 100644 index 00000000..43bcc9ca --- /dev/null +++ b/log/log.go @@ -0,0 +1,72 @@ +package log + +import ( + "path/filepath" + "sync" + "time" + + rotatelogs "github.com/lestrrat-go/file-rotatelogs" + "github.com/sirupsen/logrus" + + "github.com/vapor/config" +) + +const ( + rotationTime int64 = 86400 + maxAge int64 = 604800 +) + +var defaultFormatter = &logrus.TextFormatter{DisableColors: true} + +func InitLogFile(config *config.Config) { + hook := newBtmHook(config.LogDir()) + logrus.AddHook(hook) +} + +type BtmHook struct { + logPath string + lock *sync.Mutex +} + +func newBtmHook(logPath string) *BtmHook { + hook := &BtmHook{lock: new(sync.Mutex)} + hook.logPath = logPath + return hook +} + +// Write a log line to an io.Writer. +func (hook *BtmHook) ioWrite(entry *logrus.Entry) error { + module := "general" + if data, ok := entry.Data["module"]; ok { + module = data.(string) + } + + logPath := filepath.Join(hook.logPath, module) + writer, err := rotatelogs.New( + logPath+".%Y%m%d", + rotatelogs.WithMaxAge(time.Duration(maxAge)*time.Second), + rotatelogs.WithRotationTime(time.Duration(rotationTime)*time.Second), + ) + if err != nil { + return err + } + + msg, err := defaultFormatter.Format(entry) + if err != nil { + return err + } + + _, err = writer.Write(msg) + return err +} + +func (hook *BtmHook) Fire(entry *logrus.Entry) error { + hook.lock.Lock() + defer hook.lock.Unlock() + return hook.ioWrite(entry) +} + +// Levels returns configured log levels. +func (hook *BtmHook) Levels() []logrus.Level { + return logrus.AllLevels +} diff --git a/node/node.go b/node/node.go index 531ff832..beafcf03 100644 --- a/node/node.go +++ b/node/node.go @@ -6,7 +6,6 @@ import ( "net" "net/http" _ "net/http/pprof" - "os" "path/filepath" "reflect" @@ -26,6 +25,7 @@ import ( dbm "github.com/vapor/database/leveldb" "github.com/vapor/env" "github.com/vapor/event" + vaporLog "github.com/vapor/log" "github.com/vapor/net/websocket" "github.com/vapor/netsync" "github.com/vapor/proposal/blockproposer" @@ -66,6 +66,8 @@ func NewNode(config *cfg.Config) *Node { cmn.Exit(cmn.Fmt("Failed to load federated information:[%s]", err.Error())) } + vaporLog.InitLogFile(config) + log.WithFields(log.Fields{ "module": logModule, "pubkey": config.PrivateKey().XPub(), @@ -74,10 +76,10 @@ func NewNode(config *cfg.Config) *Node { "fed_controlprogram": hex.EncodeToString(cfg.FederationWScript(config)), }).Info() - initLogFile(config) if err := consensus.InitActiveNetParams(config.ChainID); err != nil { log.Fatalf("Failed to init ActiveNetParams:[%s]", err.Error()) } + initCommonConfig(config) // Get store @@ -188,20 +190,6 @@ func lockDataDirectory(config *cfg.Config) error { return nil } -func initLogFile(config *cfg.Config) { - if config.LogFile == "" { - return - } - cmn.EnsureDir(filepath.Dir(config.LogFile), 0700) - file, err := os.OpenFile(config.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) - if err == nil { - log.SetOutput(file) - } else { - log.WithFields(log.Fields{"module": logModule, "err": err}).Info("using default") - } - -} - func initCommonConfig(config *cfg.Config) { cfg.CommonConfig = config } diff --git a/vendor/github.com/lestrrat-go/file-rotatelogs/.gitignore b/vendor/github.com/lestrrat-go/file-rotatelogs/.gitignore new file mode 100644 index 00000000..00268614 --- /dev/null +++ b/vendor/github.com/lestrrat-go/file-rotatelogs/.gitignore @@ -0,0 +1,22 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe diff --git a/vendor/github.com/lestrrat-go/file-rotatelogs/.travis.yml b/vendor/github.com/lestrrat-go/file-rotatelogs/.travis.yml new file mode 100644 index 00000000..6e1b7a91 --- /dev/null +++ b/vendor/github.com/lestrrat-go/file-rotatelogs/.travis.yml @@ -0,0 +1,5 @@ +language: go +sudo: false +go: + - "1.10" + - tip diff --git a/vendor/github.com/lestrrat-go/file-rotatelogs/LICENSE b/vendor/github.com/lestrrat-go/file-rotatelogs/LICENSE new file mode 100644 index 00000000..9a7f25bb --- /dev/null +++ b/vendor/github.com/lestrrat-go/file-rotatelogs/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014 lestrrat + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/lestrrat-go/file-rotatelogs/README.md b/vendor/github.com/lestrrat-go/file-rotatelogs/README.md new file mode 100644 index 00000000..82a0a766 --- /dev/null +++ b/vendor/github.com/lestrrat-go/file-rotatelogs/README.md @@ -0,0 +1,236 @@ +file-rotatelogs +================== + +Provide an `io.Writer` that periodically rotates log files from within the application. Port of [File::RotateLogs](https://metacpan.org/release/File-RotateLogs) from Perl to Go. + +[![Build Status](https://travis-ci.org/lestrrat-go/file-rotatelogs.png?branch=master)](https://travis-ci.org/lestrrat-go/file-rotatelogs) + +[![GoDoc](https://godoc.org/github.com/lestrrat-go/file-rotatelogs?status.svg)](https://godoc.org/github.com/lestrrat-go/file-rotatelogs) + + +# SYNOPSIS + +```go +import ( + "log" + "net/http" + + apachelog "github.com/lestrrat-go/apache-logformat" + rotatelogs "github.com/lestrrat-go/file-rotatelogs" +) + +func main() { + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { ... }) + + logf, err := rotatelogs.New( + "/path/to/access_log.%Y%m%d%H%M", + rotatelogs.WithLinkName("/path/to/access_log"), + rotatelogs.WithMaxAge(24 * time.Hour), + rotatelogs.WithRotationTime(time.Hour), + ) + if err != nil { + log.Printf("failed to create rotatelogs: %s", err) + return + } + + // Now you must write to logf. apache-logformat library can create + // a http.Handler that only writes the approriate logs for the request + // to the given handle + http.ListenAndServe(":8080", apachelog.CombinedLog.Wrap(mux, logf)) +} +``` + +# DESCRIPTION + +When you integrate this to to you app, it automatically write to logs that +are rotated from within the app: No more disk-full alerts because you forgot +to setup logrotate! + +To install, simply issue a `go get`: + +``` +go get github.com/lestrrat-go/file-rotatelogs +``` + +It's normally expected that this library is used with some other +logging service, such as the built-in `log` library, or loggers +such as `github.com/lestrrat-go/apache-logformat`. + +```go +import( + "log" + "github.com/lestrrat-go/file-rotatelogs" +) + +func main() { + rl, _ := rotatelogs.New("/path/to/access_log.%Y%m%d%H%M") + + log.SetOutput(rl) + + /* elsewhere ... */ + log.Printf("Hello, World!") +} +``` + +OPTIONS +==== + +## Pattern (Required) + +The pattern used to generate actual log file names. You should use patterns +using the strftime (3) format. For example: + +```go + rotatelogs.New("/var/log/myapp/log.%Y%m%d") +``` + +## Clock (default: rotatelogs.Local) + +You may specify an object that implements the roatatelogs.Clock interface. +When this option is supplied, it's used to determine the current time to +base all of the calculations on. For example, if you want to base your +calculations in UTC, you may specify rotatelogs.UTC + +```go + rotatelogs.New( + "/var/log/myapp/log.%Y%m%d", + rotatelogs.WithClock(rotatelogs.UTC), + ) +``` + +## Location + +This is an alternative to the `WithClock` option. Instead of providing an +explicit clock, you can provide a location for you times. We will create +a Clock object that produces times in your specified location, and configure +the rotatelog to respect it. + +## LinkName (default: "") + +Path where a symlink for the actual log file is placed. This allows you to +always check at the same location for log files even if the logs were rotated + +```go + rotatelogs.New( + "/var/log/myapp/log.%Y%m%d", + rotatelogs.WithLinkName("/var/log/myapp/current"), + ) +``` + +``` + // Else where + $ tail -f /var/log/myapp/current +``` + +If not provided, no link will be written. + +## RotationTime (default: 86400 sec) + +Interval between file rotation. By default logs are rotated every 86400 seconds. +Note: Remember to use time.Duration values. + +```go + // Rotate every hour + rotatelogs.New( + "/var/log/myapp/log.%Y%m%d", + rotatelogs.WithRotationTime(time.Hour), + ) +``` + +## MaxAge (default: 7 days) + +Time to wait until old logs are purged. By default no logs are purged, which +certainly isn't what you want. +Note: Remember to use time.Duration values. + +```go + // Purge logs older than 1 hour + rotatelogs.New( + "/var/log/myapp/log.%Y%m%d", + rotatelogs.WithMaxAge(time.Hour), + ) +``` + +## RotationCount (default: -1) + +The number of files should be kept. By default, this option is disabled. + +Note: MaxAge should be disabled by specifing `WithMaxAge(-1)` explicitly. + +```go + // Purge logs except latest 7 files + rotatelogs.New( + "/var/log/myapp/log.%Y%m%d", + rotatelogs.WithMaxAge(-1), + rotatelogs.WithRotationCount(7), + ) +``` + +## Handler (default: nil) + +Sets the event handler to receive event notifications from the RotateLogs +object. Currently only supported event type is FiledRotated + +```go + rotatelogs.New( + "/var/log/myapp/log.%Y%m%d", + rotatelogs.Handler(rotatelogs.HandlerFunc(func(e Event) { + if e.Type() != rotatelogs.FileRotatedEventType { + return + } + + // Do what you want with the data. This is just an idea: + storeLogFileToRemoteStorage(e.(*FileRotatedEvent).PreviousFile()) + })), + ) +``` + +## ForceNewFile + +Ensure a new file is created every time New() is called. If the base file name +already exists, an implicit rotation is performed. + +```go + rotatelogs.New( + "/var/log/myapp/log.%Y%m%d", + rotatelogs.ForceNewFile(), + ) +``` + +## ForceNewFile + +Ensure a new file is created every time New() is called. If the base file name +already exists, an implicit rotation is performed. + +```go + rotatelogs.New( + "/var/log/myapp/log.%Y%m%d", + rotatelogs.ForceNewFile(), + ) +``` + +# Rotating files forcefully + +If you want to rotate files forcefully before the actual rotation time has reached, +you may use the `Rotate()` method. This method forcefully rotates the logs, but +if the generated file name clashes, then a numeric suffix is added so that +the new file will forcefully appear on disk. + +For example, suppose you had a pattern of '%Y.log' with a rotation time of +`86400` so that it only gets rotated every year, but for whatever reason you +wanted to rotate the logs now, you could install a signal handler to +trigger this rotation: + +```go +rl := rotatelogs.New(...) + +signal.Notify(ch, syscall.SIGHUP) + +go func(ch chan os.Signal) { + <-ch + rl.Rotate() +}() +``` + +And you will get a log file name in like `2018.log.1`, `2018.log.2`, etc. diff --git a/vendor/github.com/lestrrat-go/file-rotatelogs/event.go b/vendor/github.com/lestrrat-go/file-rotatelogs/event.go new file mode 100644 index 00000000..23047c42 --- /dev/null +++ b/vendor/github.com/lestrrat-go/file-rotatelogs/event.go @@ -0,0 +1,17 @@ +package rotatelogs + +func (h HandlerFunc) Handle(e Event) { + h(e) +} + +func (e *FileRotatedEvent) Type() EventType { + return FileRotatedEventType +} + +func (e *FileRotatedEvent) PreviousFile() string { + return e.prev +} + +func (e *FileRotatedEvent) CurrentFile() string { + return e.current +} diff --git a/vendor/github.com/lestrrat-go/file-rotatelogs/example_test.go b/vendor/github.com/lestrrat-go/file-rotatelogs/example_test.go new file mode 100644 index 00000000..02fb5f6b --- /dev/null +++ b/vendor/github.com/lestrrat-go/file-rotatelogs/example_test.go @@ -0,0 +1,56 @@ +package rotatelogs_test + +import ( + "fmt" + "io/ioutil" + "os" + rotatelogs "github.com/lestrrat-go/file-rotatelogs" +) + +func ExampleForceNewFile () { + logDir, err := ioutil.TempDir("", "rotatelogs_test") + if err != nil { + fmt.Println("could not create log directory ", err) + return + } + logPath := fmt.Sprintf("%s/test.log", logDir) + + for i := 0; i < 2; i++ { + writer, err := rotatelogs.New(logPath, + rotatelogs.ForceNewFile(), + ) + if err != nil { + fmt.Println("Could not open log file ", err) + return + } + + n, err := writer.Write([]byte("test")) + if err != nil || n != 4 { + fmt.Println("Write failed ", err, " number written ", n) + return + } + err = writer.Close() + if err != nil { + fmt.Println("Close failed ", err) + return + } + } + + files, err := ioutil.ReadDir(logDir) + if err != nil { + fmt.Println("ReadDir failed ", err) + return + } + for _, file := range files { + fmt.Println(file.Name(), file.Size()) + } + + err = os.RemoveAll(logDir) + if err != nil { + fmt.Println("RemoveAll failed ", err) + return + } + // OUTPUT: + // test.log 4 + // test.log.1 4 +} diff --git a/vendor/github.com/lestrrat-go/file-rotatelogs/interface.go b/vendor/github.com/lestrrat-go/file-rotatelogs/interface.go new file mode 100644 index 00000000..fcd0f58a --- /dev/null +++ b/vendor/github.com/lestrrat-go/file-rotatelogs/interface.go @@ -0,0 +1,72 @@ +package rotatelogs + +import ( + "os" + "sync" + "time" + + strftime "github.com/lestrrat-go/strftime" +) + +type Handler interface { + Handle(Event) +} + +type HandlerFunc func(Event) + +type Event interface { + Type() EventType +} + +type EventType int + +const ( + InvalidEventType EventType = iota + FileRotatedEventType +) + +type FileRotatedEvent struct { + prev string // previous filename + current string // current, new filename +} + +// RotateLogs represents a log file that gets +// automatically rotated as you write to it. +type RotateLogs struct { + clock Clock + curFn string + curBaseFn string + globPattern string + generation int + linkName string + maxAge time.Duration + mutex sync.RWMutex + eventHandler Handler + outFh *os.File + pattern *strftime.Strftime + rotationTime time.Duration + rotationCount uint + forceNewFile bool +} + +// Clock is the interface used by the RotateLogs +// object to determine the current time +type Clock interface { + Now() time.Time +} +type clockFn func() time.Time + +// UTC is an object satisfying the Clock interface, which +// returns the current time in UTC +var UTC = clockFn(func() time.Time { return time.Now().UTC() }) + +// Local is an object satisfying the Clock interface, which +// returns the current time in the local timezone +var Local = clockFn(time.Now) + +// Option is used to pass optional arguments to +// the RotateLogs constructor +type Option interface { + Name() string + Value() interface{} +} diff --git a/vendor/github.com/lestrrat-go/file-rotatelogs/internal/option/option.go b/vendor/github.com/lestrrat-go/file-rotatelogs/internal/option/option.go new file mode 100644 index 00000000..9259dc51 --- /dev/null +++ b/vendor/github.com/lestrrat-go/file-rotatelogs/internal/option/option.go @@ -0,0 +1,25 @@ +package option + +type Interface interface { + Name() string + Value() interface{} +} + +type Option struct { + name string + value interface{} +} + +func New(name string, value interface{}) *Option { + return &Option{ + name: name, + value: value, + } +} + +func (o *Option) Name() string { + return o.name +} +func (o *Option) Value() interface{} { + return o.value +} diff --git a/vendor/github.com/lestrrat-go/file-rotatelogs/internal_test.go b/vendor/github.com/lestrrat-go/file-rotatelogs/internal_test.go new file mode 100644 index 00000000..8ee39317 --- /dev/null +++ b/vendor/github.com/lestrrat-go/file-rotatelogs/internal_test.go @@ -0,0 +1,41 @@ +package rotatelogs + +import ( + "fmt" + "testing" + "time" + + "github.com/jonboulle/clockwork" + "github.com/stretchr/testify/assert" +) + +func TestGenFilename(t *testing.T) { + // Mock time + ts := []time.Time{ + time.Time{}, + (time.Time{}).Add(24 * time.Hour), + } + + for _, xt := range ts { + rl, err := New( + "/path/to/%Y/%m/%d", + WithClock(clockwork.NewFakeClockAt(xt)), + ) + if !assert.NoError(t, err, "New should succeed") { + return + } + + defer rl.Close() + + fn := rl.genFilename() + expected := fmt.Sprintf("/path/to/%04d/%02d/%02d", + xt.Year(), + xt.Month(), + xt.Day(), + ) + + if !assert.Equal(t, expected, fn) { + return + } + } +} diff --git a/vendor/github.com/lestrrat-go/file-rotatelogs/options.go b/vendor/github.com/lestrrat-go/file-rotatelogs/options.go new file mode 100644 index 00000000..49cc342b --- /dev/null +++ b/vendor/github.com/lestrrat-go/file-rotatelogs/options.go @@ -0,0 +1,82 @@ +package rotatelogs + +import ( + "time" + + "github.com/lestrrat-go/file-rotatelogs/internal/option" +) + +const ( + optkeyClock = "clock" + optkeyHandler = "handler" + optkeyLinkName = "link-name" + optkeyMaxAge = "max-age" + optkeyRotationTime = "rotation-time" + optkeyRotationCount = "rotation-count" + optkeyForceNewFile = "force-new-file" +) + +// WithClock creates a new Option that sets a clock +// that the RotateLogs object will use to determine +// the current time. +// +// By default rotatelogs.Local, which returns the +// current time in the local time zone, is used. If you +// would rather use UTC, use rotatelogs.UTC as the argument +// to this option, and pass it to the constructor. +func WithClock(c Clock) Option { + return option.New(optkeyClock, c) +} + +// WithLocation creates a new Option that sets up a +// "Clock" interface that the RotateLogs object will use +// to determine the current time. +// +// This optin works by always returning the in the given +// location. +func WithLocation(loc *time.Location) Option { + return option.New(optkeyClock, clockFn(func() time.Time { + return time.Now().In(loc) + })) +} + +// WithLinkName creates a new Option that sets the +// symbolic link name that gets linked to the current +// file name being used. +func WithLinkName(s string) Option { + return option.New(optkeyLinkName, s) +} + +// WithMaxAge creates a new Option that sets the +// max age of a log file before it gets purged from +// the file system. +func WithMaxAge(d time.Duration) Option { + return option.New(optkeyMaxAge, d) +} + +// WithRotationTime creates a new Option that sets the +// time between rotation. +func WithRotationTime(d time.Duration) Option { + return option.New(optkeyRotationTime, d) +} + +// WithRotationCount creates a new Option that sets the +// number of files should be kept before it gets +// purged from the file system. +func WithRotationCount(n uint) Option { + return option.New(optkeyRotationCount, n) +} + +// WithHandler creates a new Option that specifies the +// Handler object that gets invoked when an event occurs. +// Currently `FileRotated` event is supported +func WithHandler(h Handler) Option { + return option.New(optkeyHandler, h) +} + +// ForceNewFile ensures a new file is created every time New() +// is called. If the base file name already exists, an implicit +// rotation is performed +func ForceNewFile() Option { + return option.New(optkeyForceNewFile, true) +} diff --git a/vendor/github.com/lestrrat-go/file-rotatelogs/rotatelogs.go b/vendor/github.com/lestrrat-go/file-rotatelogs/rotatelogs.go new file mode 100644 index 00000000..3059474a --- /dev/null +++ b/vendor/github.com/lestrrat-go/file-rotatelogs/rotatelogs.go @@ -0,0 +1,366 @@ +// package rotatelogs is a port of File-RotateLogs from Perl +// (https://metacpan.org/release/File-RotateLogs), and it allows +// you to automatically rotate output files when you write to them +// according to the filename pattern that you can specify. +package rotatelogs + +import ( + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "strings" + "sync" + "time" + + strftime "github.com/lestrrat-go/strftime" + "github.com/pkg/errors" +) + +func (c clockFn) Now() time.Time { + return c() +} + +// New creates a new RotateLogs object. A log filename pattern +// must be passed. Optional `Option` parameters may be passed +func New(p string, options ...Option) (*RotateLogs, error) { + globPattern := p + for _, re := range patternConversionRegexps { + globPattern = re.ReplaceAllString(globPattern, "*") + } + + pattern, err := strftime.New(p) + if err != nil { + return nil, errors.Wrap(err, `invalid strftime pattern`) + } + + var clock Clock = Local + rotationTime := 24 * time.Hour + var rotationCount uint + var linkName string + var maxAge time.Duration + var handler Handler + var forceNewFile bool + + for _, o := range options { + switch o.Name() { + case optkeyClock: + clock = o.Value().(Clock) + case optkeyLinkName: + linkName = o.Value().(string) + case optkeyMaxAge: + maxAge = o.Value().(time.Duration) + if maxAge < 0 { + maxAge = 0 + } + case optkeyRotationTime: + rotationTime = o.Value().(time.Duration) + if rotationTime < 0 { + rotationTime = 0 + } + case optkeyRotationCount: + rotationCount = o.Value().(uint) + case optkeyHandler: + handler = o.Value().(Handler) + case optkeyForceNewFile: + forceNewFile = true + } + } + + if maxAge > 0 && rotationCount > 0 { + return nil, errors.New("options MaxAge and RotationCount cannot be both set") + } + + if maxAge == 0 && rotationCount == 0 { + // if both are 0, give maxAge a sane default + maxAge = 7 * 24 * time.Hour + } + + return &RotateLogs{ + clock: clock, + eventHandler: handler, + globPattern: globPattern, + linkName: linkName, + maxAge: maxAge, + pattern: pattern, + rotationTime: rotationTime, + rotationCount: rotationCount, + forceNewFile: forceNewFile, + }, nil +} + +func (rl *RotateLogs) genFilename() string { + now := rl.clock.Now() + + // XXX HACK: Truncate only happens in UTC semantics, apparently. + // observed values for truncating given time with 86400 secs: + // + // before truncation: 2018/06/01 03:54:54 2018-06-01T03:18:00+09:00 + // after truncation: 2018/06/01 03:54:54 2018-05-31T09:00:00+09:00 + // + // This is really annoying when we want to truncate in local time + // so we hack: we take the apparent local time in the local zone, + // and pretend that it's in UTC. do our math, and put it back to + // the local zone + var base time.Time + if now.Location() != time.UTC { + base = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second(), now.Nanosecond(), time.UTC) + base = base.Truncate(time.Duration(rl.rotationTime)) + base = time.Date(base.Year(), base.Month(), base.Day(), base.Hour(), base.Minute(), base.Second(), base.Nanosecond(), base.Location()) + } else { + base = now.Truncate(time.Duration(rl.rotationTime)) + } + return rl.pattern.FormatString(base) +} + +// Write satisfies the io.Writer interface. It writes to the +// appropriate file handle that is currently being used. +// If we have reached rotation time, the target file gets +// automatically rotated, and also purged if necessary. +func (rl *RotateLogs) Write(p []byte) (n int, err error) { + // Guard against concurrent writes + rl.mutex.Lock() + defer rl.mutex.Unlock() + + out, err := rl.getWriter_nolock(false, false) + if err != nil { + return 0, errors.Wrap(err, `failed to acquite target io.Writer`) + } + + return out.Write(p) +} + +// must be locked during this operation +func (rl *RotateLogs) getWriter_nolock(bailOnRotateFail, useGenerationalNames bool) (io.Writer, error) { + generation := rl.generation + previousFn := rl.curFn + // This filename contains the name of the "NEW" filename + // to log to, which may be newer than rl.currentFilename + baseFn := rl.genFilename() + filename := baseFn + var forceNewFile bool + if baseFn != rl.curBaseFn { + generation = 0 + // even though this is the first write after calling New(), + // check if a new file needs to be created + if rl.forceNewFile { + forceNewFile = true + } + } else { + if !useGenerationalNames { + // nothing to do + return rl.outFh, nil + } + forceNewFile = true + generation++ + } + if forceNewFile { + // A new file has been requested. Instead of just using the + // regular strftime pattern, we create a new file name using + // generational names such as "foo.1", "foo.2", "foo.3", etc + var name string + for { + if generation == 0 { + name = filename + } else { + name = fmt.Sprintf("%s.%d", filename, generation) + } + if _, err := os.Stat(name); err != nil { + filename = name + break + } + generation++ + } + } + // make sure the dir is existed, eg: + // ./foo/bar/baz/hello.log must make sure ./foo/bar/baz is existed + dirname := filepath.Dir(filename) + if err := os.MkdirAll(dirname, 0755); err != nil { + return nil, errors.Wrapf(err, "failed to create directory %s", dirname) + } + // if we got here, then we need to create a file + fh, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + return nil, errors.Errorf("failed to open file %s: %s", rl.pattern, err) + } + + if err := rl.rotate_nolock(filename); err != nil { + err = errors.Wrap(err, "failed to rotate") + if bailOnRotateFail { + // Failure to rotate is a problem, but it's really not a great + // idea to stop your application just because you couldn't rename + // your log. + // + // We only return this error when explicitly needed (as specified by bailOnRotateFail) + // + // However, we *NEED* to close `fh` here + if fh != nil { // probably can't happen, but being paranoid + fh.Close() + } + return nil, err + } + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + } + + rl.outFh.Close() + rl.outFh = fh + rl.curBaseFn = baseFn + rl.curFn = filename + rl.generation = generation + + if h := rl.eventHandler; h != nil { + go h.Handle(&FileRotatedEvent{ + prev: previousFn, + current: filename, + }) + } + return fh, nil +} + +// CurrentFileName returns the current file name that +// the RotateLogs object is writing to +func (rl *RotateLogs) CurrentFileName() string { + rl.mutex.RLock() + defer rl.mutex.RUnlock() + return rl.curFn +} + +var patternConversionRegexps = []*regexp.Regexp{ + regexp.MustCompile(`%[%+A-Za-z]`), + regexp.MustCompile(`\*+`), +} + +type cleanupGuard struct { + enable bool + fn func() + mutex sync.Mutex +} + +func (g *cleanupGuard) Enable() { + g.mutex.Lock() + defer g.mutex.Unlock() + g.enable = true +} +func (g *cleanupGuard) Run() { + g.fn() +} + +// Rotate forcefully rotates the log files. If the generated file name +// clash because file already exists, a numeric suffix of the form +// ".1", ".2", ".3" and so forth are appended to the end of the log file +// +// Thie method can be used in conjunction with a signal handler so to +// emulate servers that generate new log files when they receive a +// SIGHUP +func (rl *RotateLogs) Rotate() error { + rl.mutex.Lock() + defer rl.mutex.Unlock() + if _, err := rl.getWriter_nolock(true, true); err != nil { + return err + } + return nil +} + +func (rl *RotateLogs) rotate_nolock(filename string) error { + lockfn := filename + `_lock` + fh, err := os.OpenFile(lockfn, os.O_CREATE|os.O_EXCL, 0644) + if err != nil { + // Can't lock, just return + return err + } + + var guard cleanupGuard + guard.fn = func() { + fh.Close() + os.Remove(lockfn) + } + defer guard.Run() + + if rl.linkName != "" { + tmpLinkName := filename + `_symlink` + if err := os.Symlink(filename, tmpLinkName); err != nil { + return errors.Wrap(err, `failed to create new symlink`) + } + + if err := os.Rename(tmpLinkName, rl.linkName); err != nil { + return errors.Wrap(err, `failed to rename new symlink`) + } + } + + if rl.maxAge <= 0 && rl.rotationCount <= 0 { + return errors.New("panic: maxAge and rotationCount are both set") + } + + matches, err := filepath.Glob(rl.globPattern) + if err != nil { + return err + } + + cutoff := rl.clock.Now().Add(-1 * rl.maxAge) + var toUnlink []string + for _, path := range matches { + // Ignore lock files + if strings.HasSuffix(path, "_lock") || strings.HasSuffix(path, "_symlink") { + continue + } + + fi, err := os.Stat(path) + if err != nil { + continue + } + + fl, err := os.Lstat(path) + if err != nil { + continue + } + + if rl.maxAge > 0 && fi.ModTime().After(cutoff) { + continue + } + + if rl.rotationCount > 0 && fl.Mode()&os.ModeSymlink == os.ModeSymlink { + continue + } + toUnlink = append(toUnlink, path) + } + + if rl.rotationCount > 0 { + // Only delete if we have more than rotationCount + if rl.rotationCount >= uint(len(toUnlink)) { + return nil + } + + toUnlink = toUnlink[:len(toUnlink)-int(rl.rotationCount)] + } + + if len(toUnlink) <= 0 { + return nil + } + + guard.Enable() + go func() { + // unlink files on a separate goroutine + for _, path := range toUnlink { + os.Remove(path) + } + }() + + return nil +} + +// Close satisfies the io.Closer interface. You must +// call this method if you performed any writes to +// the object. +func (rl *RotateLogs) Close() error { + rl.mutex.Lock() + defer rl.mutex.Unlock() + + if rl.outFh == nil { + return nil + } + + rl.outFh.Close() + rl.outFh = nil + return nil +} diff --git a/vendor/github.com/lestrrat-go/file-rotatelogs/rotatelogs_test.go b/vendor/github.com/lestrrat-go/file-rotatelogs/rotatelogs_test.go new file mode 100644 index 00000000..181b2268 --- /dev/null +++ b/vendor/github.com/lestrrat-go/file-rotatelogs/rotatelogs_test.go @@ -0,0 +1,531 @@ +package rotatelogs_test + +import ( + "fmt" + "io" + "io/ioutil" + "log" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/jonboulle/clockwork" + rotatelogs "github.com/lestrrat-go/file-rotatelogs" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestSatisfiesIOWriter(t *testing.T) { + var w io.Writer + w, _ = rotatelogs.New("/foo/bar") + _ = w +} + +func TestSatisfiesIOCloser(t *testing.T) { + var c io.Closer + c, _ = rotatelogs.New("/foo/bar") + _ = c +} + +func TestLogRotate(t *testing.T) { + dir, err := ioutil.TempDir("", "file-rotatelogs-test") + if !assert.NoError(t, err, "creating temporary directory should succeed") { + return + } + defer os.RemoveAll(dir) + + // Change current time, so we can safely purge old logs + dummyTime := time.Now().Add(-7 * 24 * time.Hour) + dummyTime = dummyTime.Add(time.Duration(-1 * dummyTime.Nanosecond())) + clock := clockwork.NewFakeClockAt(dummyTime) + linkName := filepath.Join(dir, "log") + rl, err := rotatelogs.New( + filepath.Join(dir, "log%Y%m%d%H%M%S"), + rotatelogs.WithClock(clock), + rotatelogs.WithMaxAge(24*time.Hour), + rotatelogs.WithLinkName(linkName), + ) + if !assert.NoError(t, err, `rotatelogs.New should succeed`) { + return + } + defer rl.Close() + + str := "Hello, World" + n, err := rl.Write([]byte(str)) + if !assert.NoError(t, err, "rl.Write should succeed") { + return + } + + if !assert.Len(t, str, n, "rl.Write should succeed") { + return + } + + fn := rl.CurrentFileName() + if fn == "" { + t.Errorf("Could not get filename %s", fn) + } + + content, err := ioutil.ReadFile(fn) + if err != nil { + t.Errorf("Failed to read file %s: %s", fn, err) + } + + if string(content) != str { + t.Errorf(`File content does not match (was "%s")`, content) + } + + err = os.Chtimes(fn, dummyTime, dummyTime) + if err != nil { + t.Errorf("Failed to change access/modification times for %s: %s", fn, err) + } + + fi, err := os.Stat(fn) + if err != nil { + t.Errorf("Failed to stat %s: %s", fn, err) + } + + if !fi.ModTime().Equal(dummyTime) { + t.Errorf("Failed to chtime for %s (expected %s, got %s)", fn, fi.ModTime(), dummyTime) + } + + clock.Advance(time.Duration(7 * 24 * time.Hour)) + + // This next Write() should trigger Rotate() + rl.Write([]byte(str)) + newfn := rl.CurrentFileName() + if newfn == fn { + t.Errorf(`New file name and old file name should not match ("%s" != "%s")`, fn, newfn) + } + + content, err = ioutil.ReadFile(newfn) + if err != nil { + t.Errorf("Failed to read file %s: %s", newfn, err) + } + + if string(content) != str { + t.Errorf(`File content does not match (was "%s")`, content) + } + + time.Sleep(time.Second) + + // fn was declared above, before mocking CurrentTime + // Old files should have been unlinked + _, err = os.Stat(fn) + if !assert.Error(t, err, "os.Stat should have failed") { + return + } + + linkDest, err := os.Readlink(linkName) + if err != nil { + t.Errorf("Failed to readlink %s: %s", linkName, err) + } + + if linkDest != newfn { + t.Errorf(`Symlink destination does not match expected filename ("%s" != "%s")`, newfn, linkDest) + } +} + +func CreateRotationTestFile(dir string, base time.Time, d time.Duration, n int) { + timestamp := base + for i := 0; i < n; i++ { + // %Y%m%d%H%M%S + suffix := timestamp.Format("20060102150405") + path := filepath.Join(dir, "log"+suffix) + ioutil.WriteFile(path, []byte("rotation test file\n"), os.ModePerm) + os.Chtimes(path, timestamp, timestamp) + timestamp = timestamp.Add(d) + } +} + +func TestLogRotationCount(t *testing.T) { + dir, err := ioutil.TempDir("", "file-rotatelogs-rotationcount-test") + if !assert.NoError(t, err, "creating temporary directory should succeed") { + return + } + defer os.RemoveAll(dir) + + dummyTime := time.Now().Add(-7 * 24 * time.Hour) + dummyTime = dummyTime.Add(time.Duration(-1 * dummyTime.Nanosecond())) + clock := clockwork.NewFakeClockAt(dummyTime) + + t.Run("Either maxAge or rotationCount should be set", func(t *testing.T) { + rl, err := rotatelogs.New( + filepath.Join(dir, "log%Y%m%d%H%M%S"), + rotatelogs.WithClock(clock), + rotatelogs.WithMaxAge(time.Duration(0)), + rotatelogs.WithRotationCount(0), + ) + if !assert.NoError(t, err, `Both of maxAge and rotationCount is disabled`) { + return + } + defer rl.Close() + }) + + t.Run("Either maxAge or rotationCount should be set", func(t *testing.T) { + rl, err := rotatelogs.New( + filepath.Join(dir, "log%Y%m%d%H%M%S"), + rotatelogs.WithClock(clock), + rotatelogs.WithMaxAge(1), + rotatelogs.WithRotationCount(1), + ) + if !assert.Error(t, err, `Both of maxAge and rotationCount is enabled`) { + return + } + if rl != nil { + defer rl.Close() + } + }) + + t.Run("Only latest log file is kept", func(t *testing.T) { + rl, err := rotatelogs.New( + filepath.Join(dir, "log%Y%m%d%H%M%S"), + rotatelogs.WithClock(clock), + rotatelogs.WithMaxAge(-1), + rotatelogs.WithRotationCount(1), + ) + if !assert.NoError(t, err, `rotatelogs.New should succeed`) { + return + } + defer rl.Close() + + n, err := rl.Write([]byte("dummy")) + if !assert.NoError(t, err, "rl.Write should succeed") { + return + } + if !assert.Len(t, "dummy", n, "rl.Write should succeed") { + return + } + time.Sleep(time.Second) + files, err := filepath.Glob(filepath.Join(dir, "log*")) + if !assert.Equal(t, 1, len(files), "Only latest log is kept") { + return + } + }) + + t.Run("Old log files are purged except 2 log files", func(t *testing.T) { + CreateRotationTestFile(dir, dummyTime, time.Duration(time.Hour), 5) + rl, err := rotatelogs.New( + filepath.Join(dir, "log%Y%m%d%H%M%S"), + rotatelogs.WithClock(clock), + rotatelogs.WithMaxAge(-1), + rotatelogs.WithRotationCount(2), + ) + if !assert.NoError(t, err, `rotatelogs.New should succeed`) { + return + } + defer rl.Close() + + n, err := rl.Write([]byte("dummy")) + if !assert.NoError(t, err, "rl.Write should succeed") { + return + } + if !assert.Len(t, "dummy", n, "rl.Write should succeed") { + return + } + time.Sleep(time.Second) + files, err := filepath.Glob(filepath.Join(dir, "log*")) + if !assert.Equal(t, 2, len(files), "One file is kept") { + return + } + }) + +} + +func TestLogSetOutput(t *testing.T) { + dir, err := ioutil.TempDir("", "file-rotatelogs-test") + if err != nil { + t.Errorf("Failed to create temporary directory: %s", err) + } + defer os.RemoveAll(dir) + + rl, err := rotatelogs.New(filepath.Join(dir, "log%Y%m%d%H%M%S")) + if !assert.NoError(t, err, `rotatelogs.New should succeed`) { + return + } + defer rl.Close() + + log.SetOutput(rl) + defer log.SetOutput(os.Stderr) + + str := "Hello, World" + log.Print(str) + + fn := rl.CurrentFileName() + if fn == "" { + t.Errorf("Could not get filename %s", fn) + } + + content, err := ioutil.ReadFile(fn) + if err != nil { + t.Errorf("Failed to read file %s: %s", fn, err) + } + + if !strings.Contains(string(content), str) { + t.Errorf(`File content does not contain "%s" (was "%s")`, str, content) + } +} + +func TestGHIssue16(t *testing.T) { + defer func() { + if v := recover(); v != nil { + assert.NoError(t, errors.Errorf("%s", v), "error should be nil") + } + }() + + dir, err := ioutil.TempDir("", "file-rotatelogs-gh16") + if !assert.NoError(t, err, `creating temporary directory should succeed`) { + return + } + defer os.RemoveAll(dir) + + rl, err := rotatelogs.New( + filepath.Join(dir, "log%Y%m%d%H%M%S"), + rotatelogs.WithLinkName("./test.log"), + rotatelogs.WithRotationTime(10*time.Second), + rotatelogs.WithRotationCount(3), + rotatelogs.WithMaxAge(-1), + ) + if !assert.NoError(t, err, `rotatelogs.New should succeed`) { + return + } + + if !assert.NoError(t, rl.Rotate(), "rl.Rotate should succeed") { + return + } + defer rl.Close() +} + +func TestRotationGenerationalNames(t *testing.T) { + dir, err := ioutil.TempDir("", "file-rotatelogs-generational") + if !assert.NoError(t, err, `creating temporary directory should succeed`) { + return + } + defer os.RemoveAll(dir) + + t.Run("Rotate over unchanged pattern", func(t *testing.T) { + rl, err := rotatelogs.New( + filepath.Join(dir, "unchanged-pattern.log"), + ) + if !assert.NoError(t, err, `rotatelogs.New should succeed`) { + return + } + + seen := map[string]struct{}{} + for i := 0; i < 10; i++ { + rl.Write([]byte("Hello, World!")) + if !assert.NoError(t, rl.Rotate(), "rl.Rotate should succeed") { + return + } + + // Because every call to Rotate should yield a new log file, + // and the previous files already exist, the filenames should share + // the same prefix and have a unique suffix + fn := filepath.Base(rl.CurrentFileName()) + if !assert.True(t, strings.HasPrefix(fn, "unchanged-pattern.log"), "prefix for all filenames should match") { + return + } + rl.Write([]byte("Hello, World!")) + suffix := strings.TrimPrefix(fn, "unchanged-pattern.log") + expectedSuffix := fmt.Sprintf(".%d", i+1) + if !assert.True(t, suffix == expectedSuffix, "expected suffix %s found %s", expectedSuffix, suffix) { + return + } + assert.FileExists(t, rl.CurrentFileName(), "file does not exist %s", rl.CurrentFileName()) + stat, err := os.Stat(rl.CurrentFileName()) + if err == nil { + if !assert.True(t, stat.Size() == 13, "file %s size is %d, expected 13", rl.CurrentFileName(), stat.Size()) { + return + } + } else { + assert.Failf(t, "could not stat file %s", rl.CurrentFileName()) + return + } + + if _, ok := seen[suffix]; !assert.False(t, ok, `filename suffix %s should be unique`, suffix) { + return + } + seen[suffix] = struct{}{} + } + defer rl.Close() + }) + t.Run("Rotate over pattern change over every second", func(t *testing.T) { + rl, err := rotatelogs.New( + filepath.Join(dir, "every-second-pattern-%Y%m%d%H%M%S.log"), + rotatelogs.WithRotationTime(time.Nanosecond), + ) + if !assert.NoError(t, err, `rotatelogs.New should succeed`) { + return + } + + for i := 0; i < 10; i++ { + time.Sleep(time.Second) + rl.Write([]byte("Hello, World!")) + if !assert.NoError(t, rl.Rotate(), "rl.Rotate should succeed") { + return + } + + // because every new Write should yield a new logfile, + // every rorate should be create a filename ending with a .1 + if !assert.True(t, strings.HasSuffix(rl.CurrentFileName(), ".1"), "log name should end with .1") { + return + } + } + defer rl.Close() + }) +} + +type ClockFunc func() time.Time + +func (f ClockFunc) Now() time.Time { + return f() +} + +func TestGHIssue23(t *testing.T) { + dir, err := ioutil.TempDir("", "file-rotatelogs-generational") + if !assert.NoError(t, err, `creating temporary directory should succeed`) { + return + } + defer os.RemoveAll(dir) + + for _, locName := range []string{"Asia/Tokyo", "Pacific/Honolulu"} { + loc, _ := time.LoadLocation(locName) + tests := []struct { + Expected string + Clock rotatelogs.Clock + }{ + { + Expected: filepath.Join(dir, strings.ToLower(strings.Replace(locName, "/", "_", -1)) + ".201806010000.log"), + Clock: ClockFunc(func() time.Time { + return time.Date(2018, 6, 1, 3, 18, 0, 0, loc) + }), + }, + { + Expected: filepath.Join(dir, strings.ToLower(strings.Replace(locName, "/", "_", -1)) + ".201712310000.log"), + Clock: ClockFunc(func() time.Time { + return time.Date(2017, 12, 31, 23, 52, 0, 0, loc) + }), + }, + } + for _, test := range tests { + t.Run(fmt.Sprintf("location = %s, time = %s", locName, test.Clock.Now().Format(time.RFC3339)), func(t *testing.T) { + template := strings.ToLower(strings.Replace(locName, "/", "_", -1)) + ".%Y%m%d%H%M.log" + rl, err := rotatelogs.New( + filepath.Join(dir, template), + rotatelogs.WithClock(test.Clock), // we're not using WithLocation, but it's the same thing + ) + if !assert.NoError(t, err, "rotatelogs.New should succeed") { + return + } + + t.Logf("expected %s", test.Expected) + rl.Rotate() + if !assert.Equal(t, test.Expected, rl.CurrentFileName(), "file names should match") { + return + } + }) + } + } +} + +func TestForceNewFile(t *testing.T) { + dir, err := ioutil.TempDir("", "file-rotatelogs-force-new-file") + if !assert.NoError(t, err, `creating temporary directory should succeed`) { + return + } + defer os.RemoveAll(dir) + + t.Run("Force a new file", func(t *testing.T) { + + rl, err := rotatelogs.New( + filepath.Join(dir, "force-new-file.log"), + rotatelogs.ForceNewFile(), + ) + if !assert.NoError(t, err, "rotatelogs.New should succeed") { + return + } + rl.Write([]byte("Hello, World!")) + rl.Close() + + for i := 0; i < 10; i++ { + baseFn := filepath.Join(dir, "force-new-file.log") + rl, err := rotatelogs.New( + baseFn, + rotatelogs.ForceNewFile(), + ) + if !assert.NoError(t, err, "rotatelogs.New should succeed") { + return + } + rl.Write([]byte("Hello, World")) + rl.Write([]byte(fmt.Sprintf("%d", i))) + rl.Close() + + fn := filepath.Base(rl.CurrentFileName()) + suffix := strings.TrimPrefix(fn, "force-new-file.log") + expectedSuffix := fmt.Sprintf(".%d", i+1) + if !assert.True(t, suffix == expectedSuffix, "expected suffix %s found %s", expectedSuffix, suffix) { + return + } + assert.FileExists(t, rl.CurrentFileName(), "file does not exist %s", rl.CurrentFileName()) + content, err := ioutil.ReadFile(rl.CurrentFileName()) + if !assert.NoError(t, err, "ioutil.ReadFile %s should succeed", rl.CurrentFileName()) { + return + } + str := fmt.Sprintf("Hello, World%d", i) + if !assert.Equal(t, str, string(content), "read %s from file %s, not expected %s", string(content), rl.CurrentFileName(), str) { + return + } + + assert.FileExists(t, baseFn, "file does not exist %s", baseFn) + content, err = ioutil.ReadFile(baseFn) + if !assert.NoError(t, err, "ioutil.ReadFile should succeed") { + return + } + if !assert.Equal(t, "Hello, World!", string(content), "read %s from file %s, not expected Hello, World!", string(content), baseFn) { + return + } + } + + }) + + t.Run("Force a new file with Rotate", func(t *testing.T) { + + baseFn := filepath.Join(dir, "force-new-file-rotate.log") + rl, err := rotatelogs.New( + baseFn, + rotatelogs.ForceNewFile(), + ) + if !assert.NoError(t, err, "rotatelogs.New should succeed") { + return + } + rl.Write([]byte("Hello, World!")) + + for i := 0; i < 10; i++ { + if !assert.NoError(t, rl.Rotate(), "rl.Rotate should succeed") { + return + } + rl.Write([]byte("Hello, World")) + rl.Write([]byte(fmt.Sprintf("%d", i))) + assert.FileExists(t, rl.CurrentFileName(), "file does not exist %s", rl.CurrentFileName()) + content, err := ioutil.ReadFile(rl.CurrentFileName()) + if !assert.NoError(t, err, "ioutil.ReadFile %s should succeed", rl.CurrentFileName()) { + return + } + str := fmt.Sprintf("Hello, World%d", i) + if !assert.Equal(t, str, string(content), "read %s from file %s, not expected %s", string(content), rl.CurrentFileName(), str) { + return + } + + assert.FileExists(t, baseFn, "file does not exist %s", baseFn) + content, err = ioutil.ReadFile(baseFn) + if !assert.NoError(t, err, "ioutil.ReadFile should succeed") { + return + } + if !assert.Equal(t, "Hello, World!", string(content), "read %s from file %s, not expected Hello, World!", string(content), baseFn) { + return + } + } + }) +} + diff --git a/vendor/github.com/lestrrat-go/strftime/.gitignore b/vendor/github.com/lestrrat-go/strftime/.gitignore new file mode 100644 index 00000000..daf913b1 --- /dev/null +++ b/vendor/github.com/lestrrat-go/strftime/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/vendor/github.com/lestrrat-go/strftime/.travis.yml b/vendor/github.com/lestrrat-go/strftime/.travis.yml new file mode 100644 index 00000000..1c1b0322 --- /dev/null +++ b/vendor/github.com/lestrrat-go/strftime/.travis.yml @@ -0,0 +1,5 @@ +language: go +sudo: false +go: + - 1.7.x + - tip \ No newline at end of file diff --git a/vendor/github.com/lestrrat-go/strftime/LICENSE b/vendor/github.com/lestrrat-go/strftime/LICENSE new file mode 100644 index 00000000..eed69381 --- /dev/null +++ b/vendor/github.com/lestrrat-go/strftime/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 lestrrat + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/lestrrat-go/strftime/README.md b/vendor/github.com/lestrrat-go/strftime/README.md new file mode 100644 index 00000000..e7ca61f0 --- /dev/null +++ b/vendor/github.com/lestrrat-go/strftime/README.md @@ -0,0 +1,151 @@ +# strftime + +Fast strftime for Go + +[![Build Status](https://travis-ci.org/lestrrat-go/strftime.png?branch=master)](https://travis-ci.org/lestrrat-go/strftime) + +[![GoDoc](https://godoc.org/github.com/lestrrat-go/strftime?status.svg)](https://godoc.org/github.com/lestrrat-go/strftime) + +# SYNOPSIS + +```go +f := strftime.New(`.... pattern ...`) +if err := f.Format(buf, time.Now()); err != nil { + log.Println(err.Error()) +} +``` + +# DESCRIPTION + +The goals for this library are + +* Optimized for the same pattern being called repeatedly +* Be flexible about destination to write the results out +* Be as complete as possible in terms of conversion specifications + +# API + +## Format(string, time.Time) (string, error) + +Takes the pattern and the time, and formats it. This function is a utility function that recompiles the pattern every time the function is called. If you know beforehand that you will be formatting the same pattern multiple times, consider using `New` to create a `Strftime` object and reuse it. + +## New(string) (\*Strftime, error) + +Takes the pattern and creates a new `Strftime` object. + +## obj.Pattern() string + +Returns the pattern string used to create this `Strftime` object + +## obj.Format(io.Writer, time.Time) error + +Formats the time according to the pre-compiled pattern, and writes the result to the specified `io.Writer` + +## obj.FormatString(time.Time) string + +Formats the time according to the pre-compiled pattern, and returns the result string. + +# SUPPORTED CONVERSION SPECIFICATIONS + +| pattern | description | +|:--------|:------------| +| %A | national representation of the full weekday name | +| %a | national representation of the abbreviated weekday | +| %B | national representation of the full month name | +| %b | national representation of the abbreviated month name | +| %C | (year / 100) as decimal number; single digits are preceded by a zero | +| %c | national representation of time and date | +| %D | equivalent to %m/%d/%y | +| %d | day of the month as a decimal number (01-31) | +| %e | the day of the month as a decimal number (1-31); single digits are preceded by a blank | +| %F | equivalent to %Y-%m-%d | +| %H | the hour (24-hour clock) as a decimal number (00-23) | +| %h | same as %b | +| %I | the hour (12-hour clock) as a decimal number (01-12) | +| %j | the day of the year as a decimal number (001-366) | +| %k | the hour (24-hour clock) as a decimal number (0-23); single digits are preceded by a blank | +| %l | the hour (12-hour clock) as a decimal number (1-12); single digits are preceded by a blank | +| %M | the minute as a decimal number (00-59) | +| %m | the month as a decimal number (01-12) | +| %n | a newline | +| %p | national representation of either "ante meridiem" (a.m.) or "post meridiem" (p.m.) as appropriate. | +| %R | equivalent to %H:%M | +| %r | equivalent to %I:%M:%S %p | +| %S | the second as a decimal number (00-60) | +| %T | equivalent to %H:%M:%S | +| %t | a tab | +| %U | the week number of the year (Sunday as the first day of the week) as a decimal number (00-53) | +| %u | the weekday (Monday as the first day of the week) as a decimal number (1-7) | +| %V | the week number of the year (Monday as the first day of the week) as a decimal number (01-53) | +| %v | equivalent to %e-%b-%Y | +| %W | the week number of the year (Monday as the first day of the week) as a decimal number (00-53) | +| %w | the weekday (Sunday as the first day of the week) as a decimal number (0-6) | +| %X | national representation of the time | +| %x | national representation of the date | +| %Y | the year with century as a decimal number | +| %y | the year without century as a decimal number (00-99) | +| %Z | the time zone name | +| %z | the time zone offset from UTC | +| %% | a '%' | + +# PERFORMANCE / OTHER LIBRARIES + +The following benchmarks were run separately because some libraries were using cgo on specific platforms (notabley, the fastly version) + +``` +// On my OS X 10.11.6, 2.9 GHz Intel Core i5, 16GB memory. +// go version go1.8rc1 darwin/amd64 +hummingbird% go test -tags bench -benchmem -bench . + +BenchmarkTebeka-4 300000 4469 ns/op 288 B/op 21 allocs/op +BenchmarkJehiah-4 1000000 1931 ns/op 256 B/op 17 allocs/op +BenchmarkFastly-4 2000000 724 ns/op 80 B/op 5 allocs/op +BenchmarkLestrrat-4 1000000 1572 ns/op 240 B/op 3 allocs/op +BenchmarkLestrratCachedString-4 3000000 548 ns/op 128 B/op 2 allocs/op +BenchmarkLestrratCachedWriter-4 500000 2519 ns/op 192 B/op 3 allocs/op +PASS +ok github.com/lestrrat-go/strftime 22.900s +``` + +``` +// On a host on Google Cloud Platform, machine-type: n1-standard-4 (vCPU x 4, memory: 15GB) +// Linux 3.16.0-4-amd64 #1 SMP Debian 3.16.36-1+deb8u2 (2016-10-19) x86_64 GNU/Linux +// go version go1.8rc1 linux/amd64 +hummingbird% go test -tags bench -benchmem -bench . + +BenchmarkTebeka-4 500000 3904 ns/op 288 B/op 21 allocs/op +BenchmarkJehiah-4 1000000 1665 ns/op 256 B/op 17 allocs/op +BenchmarkFastly-4 1000000 2134 ns/op 192 B/op 13 allocs/op +BenchmarkLestrrat-4 1000000 1327 ns/op 240 B/op 3 allocs/op +BenchmarkLestrratCachedString-4 3000000 498 ns/op 128 B/op 2 allocs/op +BenchmarkLestrratCachedWriter-4 1000000 3390 ns/op 192 B/op 3 allocs/op +PASS +ok github.com/lestrrat-go/strftime 44.854s +``` + +This library is much faster than other libraries *IF* you can reuse the format pattern. + +Here's the annotated list from the benchmark results. You can clearly see that (re)using a `Strftime` object +and producing a string is the fastest. Writing to an `io.Writer` seems a bit sluggish, but since +the one producing the string is doing almost exactly the same thing, we believe this is purely the overhead of +writing to an `io.Writer` + +| Import Path | Score | Note | +|:------------------------------------|--------:|:--------------------------------| +| github.com/lestrrat-go/strftime | 3000000 | Using `FormatString()` (cached) | +| github.com/fastly/go-utils/strftime | 2000000 | Pure go version on OS X | +| github.com/lestrrat-go/strftime | 1000000 | Using `Format()` (NOT cached) | +| github.com/jehiah/go-strftime | 1000000 | | +| github.com/fastly/go-utils/strftime | 1000000 | cgo version on Linux | +| github.com/lestrrat-go/strftime | 500000 | Using `Format()` (cached) | +| github.com/tebeka/strftime | 300000 | | + +However, depending on your pattern, this speed may vary. If you find a particular pattern that seems sluggish, +please send in patches or tests. + +Please also note that this benchmark only uses the subset of conversion specifications that are supported by *ALL* of the libraries compared. + +Somethings to consider when making performance comparisons in the future: + +* Can it write to io.Writer? +* Which `%specification` does it handle? diff --git a/vendor/github.com/lestrrat-go/strftime/internal_test.go b/vendor/github.com/lestrrat-go/strftime/internal_test.go new file mode 100644 index 00000000..eaee1d52 --- /dev/null +++ b/vendor/github.com/lestrrat-go/strftime/internal_test.go @@ -0,0 +1,28 @@ +package strftime + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCombine(t *testing.T) { + { + s, _ := New(`%A foo`) + if !assert.Equal(t, 1, len(s.compiled), "there are 1 element") { + return + } + } + { + s, _ := New(`%A 100`) + if !assert.Equal(t, 2, len(s.compiled), "there are two elements") { + return + } + } + { + s, _ := New(`%A Mon`) + if !assert.Equal(t, 2, len(s.compiled), "there are two elements") { + return + } + } +} diff --git a/vendor/github.com/lestrrat-go/strftime/strftime.go b/vendor/github.com/lestrrat-go/strftime/strftime.go new file mode 100644 index 00000000..bc34eb49 --- /dev/null +++ b/vendor/github.com/lestrrat-go/strftime/strftime.go @@ -0,0 +1,219 @@ +package strftime + +import ( + "io" + "strings" + "time" + + "github.com/pkg/errors" +) + +var directives = map[byte]appender{ + 'A': timefmt("Monday"), + 'a': timefmt("Mon"), + 'B': timefmt("January"), + 'b': timefmt("Jan"), + 'C': ¢ury{}, + 'c': timefmt("Mon Jan _2 15:04:05 2006"), + 'D': timefmt("01/02/06"), + 'd': timefmt("02"), + 'e': timefmt("_2"), + 'F': timefmt("2006-01-02"), + 'H': timefmt("15"), + 'h': timefmt("Jan"), // same as 'b' + 'I': timefmt("3"), + 'j': &dayofyear{}, + 'k': hourwblank(false), + 'l': hourwblank(true), + 'M': timefmt("04"), + 'm': timefmt("01"), + 'n': verbatim("\n"), + 'p': timefmt("PM"), + 'R': timefmt("15:04"), + 'r': timefmt("3:04:05 PM"), + 'S': timefmt("05"), + 'T': timefmt("15:04:05"), + 't': verbatim("\t"), + 'U': weeknumberOffset(0), // week number of the year, Sunday first + 'u': weekday(1), + 'V': &weeknumber{}, + 'v': timefmt("_2-Jan-2006"), + 'W': weeknumberOffset(1), // week number of the year, Monday first + 'w': weekday(0), + 'X': timefmt("15:04:05"), // national representation of the time XXX is this correct? + 'x': timefmt("01/02/06"), // national representation of the date XXX is this correct? + 'Y': timefmt("2006"), // year with century + 'y': timefmt("06"), // year w/o century + 'Z': timefmt("MST"), // time zone name + 'z': timefmt("-0700"), // time zone ofset from UTC + '%': verbatim("%"), +} + +type combiningAppend struct { + list appenderList + prev appender + prevCanCombine bool +} + +func (ca *combiningAppend) Append(w appender) { + if ca.prevCanCombine { + if wc, ok := w.(combiner); ok && wc.canCombine() { + ca.prev = ca.prev.(combiner).combine(wc) + ca.list[len(ca.list)-1] = ca.prev + return + } + } + + ca.list = append(ca.list, w) + ca.prev = w + ca.prevCanCombine = false + if comb, ok := w.(combiner); ok { + if comb.canCombine() { + ca.prevCanCombine = true + } + } +} + +func compile(wl *appenderList, p string) error { + var ca combiningAppend + for l := len(p); l > 0; l = len(p) { + i := strings.IndexByte(p, '%') + if i < 0 { + ca.Append(verbatim(p)) + // this is silly, but I don't trust break keywords when there's a + // possibility of this piece of code being rearranged + p = p[l:] + continue + } + if i == l-1 { + return errors.New(`stray % at the end of pattern`) + } + + // we found a '%'. we need the next byte to decide what to do next + // we already know that i < l - 1 + // everything up to the i is verbatim + if i > 0 { + ca.Append(verbatim(p[:i])) + p = p[i:] + } + + directive, ok := directives[p[1]] + if !ok { + return errors.Errorf(`unknown time format specification '%c'`, p[1]) + } + ca.Append(directive) + p = p[2:] + } + + *wl = ca.list + + return nil +} + +// Format takes the format `s` and the time `t` to produce the +// format date/time. Note that this function re-compiles the +// pattern every time it is called. +// +// If you know beforehand that you will be reusing the pattern +// within your application, consider creating a `Strftime` object +// and reusing it. +func Format(p string, t time.Time) (string, error) { + var dst []byte + // TODO: optimize for 64 byte strings + dst = make([]byte, 0, len(p)+10) + // Compile, but execute as we go + for l := len(p); l > 0; l = len(p) { + i := strings.IndexByte(p, '%') + if i < 0 { + dst = append(dst, p...) + // this is silly, but I don't trust break keywords when there's a + // possibility of this piece of code being rearranged + p = p[l:] + continue + } + if i == l-1 { + return "", errors.New(`stray % at the end of pattern`) + } + + // we found a '%'. we need the next byte to decide what to do next + // we already know that i < l - 1 + // everything up to the i is verbatim + if i > 0 { + dst = append(dst, p[:i]...) + p = p[i:] + } + + directive, ok := directives[p[1]] + if !ok { + return "", errors.Errorf(`unknown time format specification '%c'`, p[1]) + } + dst = directive.Append(dst, t) + p = p[2:] + } + + return string(dst), nil +} + +// Strftime is the object that represents a compiled strftime pattern +type Strftime struct { + pattern string + compiled appenderList +} + +// New creates a new Strftime object. If the compilation fails, then +// an error is returned in the second argument. +func New(f string) (*Strftime, error) { + var wl appenderList + if err := compile(&wl, f); err != nil { + return nil, errors.Wrap(err, `failed to compile format`) + } + return &Strftime{ + pattern: f, + compiled: wl, + }, nil +} + +// Pattern returns the original pattern string +func (f *Strftime) Pattern() string { + return f.pattern +} + +// Format takes the destination `dst` and time `t`. It formats the date/time +// using the pre-compiled pattern, and outputs the results to `dst` +func (f *Strftime) Format(dst io.Writer, t time.Time) error { + const bufSize = 64 + var b []byte + max := len(f.pattern) + 10 + if max < bufSize { + var buf [bufSize]byte + b = buf[:0] + } else { + b = make([]byte, 0, max) + } + if _, err := dst.Write(f.format(b, t)); err != nil { + return err + } + return nil +} + +func (f *Strftime) format(b []byte, t time.Time) []byte { + for _, w := range f.compiled { + b = w.Append(b, t) + } + return b +} + +// FormatString takes the time `t` and formats it, returning the +// string containing the formated data. +func (f *Strftime) FormatString(t time.Time) string { + const bufSize = 64 + var b []byte + max := len(f.pattern) + 10 + if max < bufSize { + var buf [bufSize]byte + b = buf[:0] + } else { + b = make([]byte, 0, max) + } + return string(f.format(b, t)) +} diff --git a/vendor/github.com/lestrrat-go/strftime/strftime_bench_test.go b/vendor/github.com/lestrrat-go/strftime/strftime_bench_test.go new file mode 100644 index 00000000..643f370d --- /dev/null +++ b/vendor/github.com/lestrrat-go/strftime/strftime_bench_test.go @@ -0,0 +1,80 @@ +// +build bench + +package strftime_test + +import ( + "bytes" + "log" + "net/http" + _ "net/http/pprof" + "testing" + "time" + + jehiah "github.com/jehiah/go-strftime" + fastly "github.com/fastly/go-utils/strftime" + lestrrat "github.com/lestrrat-go/strftime" + tebeka "github.com/tebeka/strftime" +) + +func init() { + go func() { + log.Println(http.ListenAndServe("localhost:8080", nil)) + }() +} + +const benchfmt = `%A %a %B %b %d %H %I %M %m %p %S %Y %y %Z` + +func BenchmarkTebeka(b *testing.B) { + var t time.Time + for i := 0; i < b.N; i++ { + tebeka.Format(benchfmt, t) + } +} + +func BenchmarkJehiah(b *testing.B) { + // Grr, uses byte slices, and does it faster, but with more allocs + var t time.Time + for i := 0; i < b.N; i++ { + jehiah.Format(benchfmt, t) + } +} + +func BenchmarkFastly(b *testing.B) { + var t time.Time + for i := 0; i < b.N; i++ { + fastly.Strftime(benchfmt, t) + } +} + +func BenchmarkLestrrat(b *testing.B) { + var t time.Time + for i := 0; i < b.N; i++ { + lestrrat.Format(benchfmt, t) + } +} + +func BenchmarkLestrratCachedString(b *testing.B) { + var t time.Time + f, _ := lestrrat.New(benchfmt) + // This benchmark does not take into effect the compilation time + for i := 0; i < b.N; i++ { + f.FormatString(t) + } +} + +func BenchmarkLestrratCachedWriter(b *testing.B) { + var t time.Time + f, _ := lestrrat.New(benchfmt) + var buf bytes.Buffer + b.ResetTimer() + + // This benchmark does not take into effect the compilation time + // nor the buffer reset time + for i := 0; i < b.N; i++ { + b.StopTimer() + buf.Reset() + b.StartTimer() + f.Format(&buf, t) + f.FormatString(t) + } +} diff --git a/vendor/github.com/lestrrat-go/strftime/strftime_test.go b/vendor/github.com/lestrrat-go/strftime/strftime_test.go new file mode 100644 index 00000000..41f25be0 --- /dev/null +++ b/vendor/github.com/lestrrat-go/strftime/strftime_test.go @@ -0,0 +1,148 @@ +package strftime_test + +import ( + "os" + "testing" + "time" + + envload "github.com/lestrrat-go/envload" + "github.com/lestrrat-go/strftime" + "github.com/stretchr/testify/assert" +) + +var ref = time.Unix(1136239445, 0).UTC() + +func TestExclusion(t *testing.T) { + s, err := strftime.New("%p PM") + if !assert.NoError(t, err, `strftime.New should succeed`) { + return + } + + var tm time.Time + if !assert.Equal(t, "AM PM", s.FormatString(tm)) { + return + } +} + +func TestInvalid(t *testing.T) { + _, err := strftime.New("%") + if !assert.Error(t, err, `strftime.New should return error`) { + return + } + + _, err = strftime.New(" %") + if !assert.Error(t, err, `strftime.New should return error`) { + return + } + _, err = strftime.New(" % ") + if !assert.Error(t, err, `strftime.New should return error`) { + return + } +} + +func TestFormat(t *testing.T) { + l := envload.New() + defer l.Restore() + + os.Setenv("LC_ALL", "C") + + s, err := strftime.Format(`%A %a %B %b %C %c %D %d %e %F %H %h %I %j %k %l %M %m %n %p %R %r %S %T %t %U %u %V %v %W %w %X %x %Y %y %Z %z`, ref) + if !assert.NoError(t, err, `strftime.Format succeeds`) { + return + } + + if !assert.Equal(t, "Monday Mon January Jan 20 Mon Jan 2 22:04:05 2006 01/02/06 02 2 2006-01-02 22 Jan 10 002 22 10 04 01 \n PM 22:04 10:04:05 PM 05 22:04:05 \t 01 1 01 2-Jan-2006 01 1 22:04:05 01/02/06 2006 06 UTC +0000", s, `formatted result matches`) { + return + } +} + +func TestFormatBlanks(t *testing.T) { + l := envload.New() + defer l.Restore() + + os.Setenv("LC_ALL", "C") + + { + dt := time.Date(1, 1, 1, 18, 0, 0, 0, time.UTC) + s, err := strftime.Format("%l", dt) + if !assert.NoError(t, err, `strftime.Format succeeds`) { + return + } + + if !assert.Equal(t, " 6", s, "leading blank is properly set") { + return + } + } + { + dt := time.Date(1, 1, 1, 6, 0, 0, 0, time.UTC) + s, err := strftime.Format("%k", dt) + if !assert.NoError(t, err, `strftime.Format succeeds`) { + return + } + + if !assert.Equal(t, " 6", s, "leading blank is properly set") { + return + } + } +} + +func TestFormatZeropad(t *testing.T) { + l := envload.New() + defer l.Restore() + + os.Setenv("LC_ALL", "C") + + { + dt := time.Date(1, 1, 1, 1, 0, 0, 0, time.UTC) + s, err := strftime.Format("%j", dt) + if !assert.NoError(t, err, `strftime.Format succeeds`) { + return + } + + if !assert.Equal(t, "001", s, "padding is properly set") { + return + } + } + { + dt := time.Date(1, 1, 10, 6, 0, 0, 0, time.UTC) + s, err := strftime.Format("%j", dt) + if !assert.NoError(t, err, `strftime.Format succeeds`) { + return + } + + if !assert.Equal(t, "010", s, "padding is properly set") { + return + } + } + { + dt := time.Date(1, 6, 1, 6, 0, 0, 0, time.UTC) + s, err := strftime.Format("%j", dt) + if !assert.NoError(t, err, `strftime.Format succeeds`) { + return + } + + if !assert.Equal(t, "152", s, "padding is properly set") { + return + } + } + { + dt := time.Date(100, 1, 1, 1, 0, 0, 0, time.UTC) + s, err := strftime.Format("%C", dt) + if !assert.NoError(t, err, `strftime.Format succeeds`) { + return + } + + if !assert.Equal(t, "01", s, "padding is properly set") { + return + } + } +} + +func TestGHIssue5(t *testing.T) { + const expected = `apm-test/logs/apm.log.01000101` + p, _ := strftime.New("apm-test/logs/apm.log.%Y%m%d") + dt := time.Date(100, 1, 1, 1, 0, 0, 0, time.UTC) + if !assert.Equal(t, expected, p.FormatString(dt), `patterns including 'pm' should be treated as verbatim formatter`) { + return + } +} diff --git a/vendor/github.com/lestrrat-go/strftime/writer.go b/vendor/github.com/lestrrat-go/strftime/writer.go new file mode 100644 index 00000000..b52b29ea --- /dev/null +++ b/vendor/github.com/lestrrat-go/strftime/writer.go @@ -0,0 +1,169 @@ +package strftime + +import ( + "strconv" + "strings" + "time" +) + +type appender interface { + Append([]byte, time.Time) []byte +} + +type appenderList []appender + +// does the time.Format thing +type timefmtw struct { + s string +} + +func timefmt(s string) *timefmtw { + return &timefmtw{s: s} +} + +func (v timefmtw) Append(b []byte, t time.Time) []byte { + return t.AppendFormat(b, v.s) +} + +func (v timefmtw) str() string { + return v.s +} + +func (v timefmtw) canCombine() bool { + return true +} + +func (v timefmtw) combine(w combiner) appender { + return timefmt(v.s + w.str()) +} + +type verbatimw struct { + s string +} + +func verbatim(s string) *verbatimw { + return &verbatimw{s: s} +} + +func (v verbatimw) Append(b []byte, _ time.Time) []byte { + return append(b, v.s...) +} + +func (v verbatimw) canCombine() bool { + return canCombine(v.s) +} + +func (v verbatimw) combine(w combiner) appender { + if _, ok := w.(*timefmtw); ok { + return timefmt(v.s + w.str()) + } + return verbatim(v.s + w.str()) +} + +func (v verbatimw) str() string { + return v.s +} + +// These words below, as well as any decimal character +var combineExclusion = []string{ + "Mon", + "Monday", + "Jan", + "January", + "MST", + "PM", + "pm", +} + +func canCombine(s string) bool { + if strings.ContainsAny(s, "0123456789") { + return false + } + for _, word := range combineExclusion { + if strings.Contains(s, word) { + return false + } + } + return true +} + +type combiner interface { + canCombine() bool + combine(combiner) appender + str() string +} + +type century struct{} + +func (v century) Append(b []byte, t time.Time) []byte { + n := t.Year() / 100 + if n < 10 { + b = append(b, '0') + } + return append(b, strconv.Itoa(n)...) +} + +type weekday int + +func (v weekday) Append(b []byte, t time.Time) []byte { + n := int(t.Weekday()) + if n < int(v) { + n += 7 + } + return append(b, byte(n+48)) +} + +type weeknumberOffset int + +func (v weeknumberOffset) Append(b []byte, t time.Time) []byte { + yd := t.YearDay() + offset := int(t.Weekday()) - int(v) + if offset < 0 { + offset += 7 + } + + if yd < offset { + return append(b, '0', '0') + } + + n := ((yd - offset) / 7) + 1 + if n < 10 { + b = append(b, '0') + } + return append(b, strconv.Itoa(n)...) +} + +type weeknumber struct{} + +func (v weeknumber) Append(b []byte, t time.Time) []byte { + _, n := t.ISOWeek() + if n < 10 { + b = append(b, '0') + } + return append(b, strconv.Itoa(n)...) +} + +type dayofyear struct{} + +func (v dayofyear) Append(b []byte, t time.Time) []byte { + n := t.YearDay() + if n < 10 { + b = append(b, '0', '0') + } else if n < 100 { + b = append(b, '0') + } + return append(b, strconv.Itoa(n)...) +} + +type hourwblank bool + +func (v hourwblank) Append(b []byte, t time.Time) []byte { + h := t.Hour() + if bool(v) && h > 12 { + h = h - 12 + } + if h < 10 { + b = append(b, ' ') + } + return append(b, strconv.Itoa(h)...) +}