DBBackend: "leveldb",
DBPath: "data",
KeysPath: "keystore",
+ LogFile: "log",
PrivateKeyFile: "node_key.txt",
FederationFileName: "federation.json",
}
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)
}
--- /dev/null
+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
+}
"net"
"net/http"
_ "net/http/pprof"
- "os"
"path/filepath"
"reflect"
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"
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(),
"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
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
}
--- /dev/null
+# 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
--- /dev/null
+language: go
+sudo: false
+go:
+ - "1.10"
+ - tip
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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
+}
--- /dev/null
+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
+}
--- /dev/null
+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{}
+}
--- /dev/null
+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
+}
--- /dev/null
+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
+ }
+ }
+}
--- /dev/null
+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)
+}
--- /dev/null
+// 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
+}
--- /dev/null
+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
+ }
+ }
+ })
+}
+
--- /dev/null
+# 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
--- /dev/null
+language: go
+sudo: false
+go:
+ - 1.7.x
+ - tip
\ No newline at end of file
--- /dev/null
+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.
--- /dev/null
+# 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?
--- /dev/null
+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
+ }
+ }
+}
--- /dev/null
+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))
+}
--- /dev/null
+// +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)
+ }
+}
--- /dev/null
+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
+ }
+}
--- /dev/null
+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)...)
+}