OSDN Git Service

log into file (#357)
authorwyjDoraemon <46176410+wyjDoraemon@users.noreply.github.com>
Mon, 29 Jul 2019 02:21:04 +0000 (10:21 +0800)
committerPaladz <yzhu101@uottawa.ca>
Mon, 29 Jul 2019 02:21:04 +0000 (10:21 +0800)
* log into file

* fig log

* fix

* fix log into file

* ignore

* vendor lestrrat

* fix grammar

* final

* fix

* fix fix

* fix

24 files changed:
config/config.go
log/log.go [new file with mode: 0644]
node/node.go
vendor/github.com/lestrrat-go/file-rotatelogs/.gitignore [new file with mode: 0644]
vendor/github.com/lestrrat-go/file-rotatelogs/.travis.yml [new file with mode: 0644]
vendor/github.com/lestrrat-go/file-rotatelogs/LICENSE [new file with mode: 0644]
vendor/github.com/lestrrat-go/file-rotatelogs/README.md [new file with mode: 0644]
vendor/github.com/lestrrat-go/file-rotatelogs/event.go [new file with mode: 0644]
vendor/github.com/lestrrat-go/file-rotatelogs/example_test.go [new file with mode: 0644]
vendor/github.com/lestrrat-go/file-rotatelogs/interface.go [new file with mode: 0644]
vendor/github.com/lestrrat-go/file-rotatelogs/internal/option/option.go [new file with mode: 0644]
vendor/github.com/lestrrat-go/file-rotatelogs/internal_test.go [new file with mode: 0644]
vendor/github.com/lestrrat-go/file-rotatelogs/options.go [new file with mode: 0644]
vendor/github.com/lestrrat-go/file-rotatelogs/rotatelogs.go [new file with mode: 0644]
vendor/github.com/lestrrat-go/file-rotatelogs/rotatelogs_test.go [new file with mode: 0644]
vendor/github.com/lestrrat-go/strftime/.gitignore [new file with mode: 0644]
vendor/github.com/lestrrat-go/strftime/.travis.yml [new file with mode: 0644]
vendor/github.com/lestrrat-go/strftime/LICENSE [new file with mode: 0644]
vendor/github.com/lestrrat-go/strftime/README.md [new file with mode: 0644]
vendor/github.com/lestrrat-go/strftime/internal_test.go [new file with mode: 0644]
vendor/github.com/lestrrat-go/strftime/strftime.go [new file with mode: 0644]
vendor/github.com/lestrrat-go/strftime/strftime_bench_test.go [new file with mode: 0644]
vendor/github.com/lestrrat-go/strftime/strftime_test.go [new file with mode: 0644]
vendor/github.com/lestrrat-go/strftime/writer.go [new file with mode: 0644]

index 8b5ede8..02d2c7c 100644 (file)
@@ -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 (file)
index 0000000..43bcc9c
--- /dev/null
@@ -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
+}
index 531ff83..beafcf0 100644 (file)
@@ -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 (file)
index 0000000..0026861
--- /dev/null
@@ -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 (file)
index 0000000..6e1b7a9
--- /dev/null
@@ -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 (file)
index 0000000..9a7f25b
--- /dev/null
@@ -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 (file)
index 0000000..82a0a76
--- /dev/null
@@ -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 (file)
index 0000000..23047c4
--- /dev/null
@@ -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 (file)
index 0000000..02fb5f6
--- /dev/null
@@ -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 (file)
index 0000000..fcd0f58
--- /dev/null
@@ -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 (file)
index 0000000..9259dc5
--- /dev/null
@@ -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 (file)
index 0000000..8ee3931
--- /dev/null
@@ -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 (file)
index 0000000..49cc342
--- /dev/null
@@ -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 (file)
index 0000000..3059474
--- /dev/null
@@ -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 (file)
index 0000000..181b226
--- /dev/null
@@ -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 (file)
index 0000000..daf913b
--- /dev/null
@@ -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 (file)
index 0000000..1c1b032
--- /dev/null
@@ -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 (file)
index 0000000..eed6938
--- /dev/null
@@ -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 (file)
index 0000000..e7ca61f
--- /dev/null
@@ -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 .
+<snip>
+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 <snip> 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 .
+<snip>
+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 (file)
index 0000000..eaee1d5
--- /dev/null
@@ -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 (file)
index 0000000..bc34eb4
--- /dev/null
@@ -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': &century{},
+       '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 (file)
index 0000000..643f370
--- /dev/null
@@ -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 (file)
index 0000000..41f25be
--- /dev/null
@@ -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 (file)
index 0000000..b52b29e
--- /dev/null
@@ -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)...)
+}