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)...)
-}