mirror of
https://gitee.com/johng/gf
synced 2026-06-06 02:25:47 +08:00
add file rotation feature for package glog; improve gpool/gfpool; fix issue in gfile.MTimeMillisecond
This commit is contained in:
@ -33,9 +33,9 @@ type SortedArray struct {
|
||||
// NewSortedArray creates and returns an empty sorted array.
|
||||
// The parameter <safe> is used to specify whether using array in concurrent-safety, which is false in default.
|
||||
// The parameter <comparator> used to compare values to sort in array,
|
||||
// if it returns value < 0, means v1 < v2;
|
||||
// if it returns value = 0, means v1 = v2;
|
||||
// if it returns value > 0, means v1 > v2;
|
||||
// if it returns value < 0, means v1 < v2; the v1 will be inserted before v2;
|
||||
// if it returns value = 0, means v1 = v2; the v1 will be replaced by v2;
|
||||
// if it returns value > 0, means v1 > v2; the v1 will be inserted after v2;
|
||||
func NewSortedArray(comparator func(a, b interface{}) int, safe ...bool) *SortedArray {
|
||||
return NewSortedArraySize(0, comparator, safe...)
|
||||
}
|
||||
|
||||
@ -721,7 +721,7 @@ func (c *Core) MarshalJSON() ([]byte, error) {
|
||||
// writeSqlToLogger outputs the sql object to logger.
|
||||
// It is enabled when configuration "debug" is true.
|
||||
func (c *Core) writeSqlToLogger(v *Sql) {
|
||||
s := fmt.Sprintf("[%d ms] %s", v.End-v.Start, v.Format)
|
||||
s := fmt.Sprintf("[%3d ms] %s", v.End-v.Start, v.Format)
|
||||
if v.Error != nil {
|
||||
s += "\nError: " + v.Error.Error()
|
||||
c.logger.StackWithFilter(gPATH_FILTER_KEY).Error(s)
|
||||
|
||||
@ -62,6 +62,7 @@ func init() {
|
||||
} else {
|
||||
db = r
|
||||
}
|
||||
db.SetDebug(true)
|
||||
schemaTemplate := "CREATE DATABASE IF NOT EXISTS `%s` CHARACTER SET UTF8"
|
||||
if _, err := db.Exec(fmt.Sprintf(schemaTemplate, SCHEMA1)); err != nil {
|
||||
gtest.Error(err)
|
||||
|
||||
@ -9,10 +9,11 @@ package gcompress
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"github.com/gogf/gf/os/gfile"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Gzip compresses <data> with gzip algorithm.
|
||||
// Gzip compresses <data> using gzip algorithm.
|
||||
// The optional parameter <level> specifies the compression level from
|
||||
// 1 to 9 which means from none to the best compression.
|
||||
//
|
||||
@ -38,6 +39,38 @@ func Gzip(data []byte, level ...int) ([]byte, error) {
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// GzipFile compresses the file <src> to <dst> using gzip algorithm.
|
||||
func GzipFile(src, dst string, level ...int) error {
|
||||
var writer *gzip.Writer
|
||||
var err error
|
||||
srcFile, err := gfile.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
dstFile, err := gfile.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
if len(level) > 0 {
|
||||
writer, err = gzip.NewWriterLevel(dstFile, level[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
writer = gzip.NewWriter(dstFile)
|
||||
}
|
||||
defer writer.Close()
|
||||
|
||||
_, err = io.Copy(writer, srcFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnGzip decompresses <data> with gzip algorithm.
|
||||
func UnGzip(data []byte) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
@ -53,3 +86,28 @@ func UnGzip(data []byte) ([]byte, error) {
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// UnGzip decompresses file <src> to <dst> using gzip algorithm.
|
||||
func UnGzipFile(src, dst string) error {
|
||||
srcFile, err := gfile.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
dstFile, err := gfile.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
reader, err := gzip.NewReader(srcFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
if _, err = io.Copy(dstFile, reader); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -7,6 +7,9 @@
|
||||
package gcompress_test
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/debug/gdebug"
|
||||
"github.com/gogf/gf/os/gfile"
|
||||
"github.com/gogf/gf/os/gtime"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/encoding/gcompress"
|
||||
@ -26,14 +29,37 @@ func Test_Gzip_UnGzip(t *testing.T) {
|
||||
0x24, 0xa8, 0xd1, 0x0d, 0x00,
|
||||
0x00, 0x00,
|
||||
}
|
||||
gtest.Case(t, func() {
|
||||
arr := []byte(src)
|
||||
data, _ := gcompress.Gzip(arr)
|
||||
gtest.Assert(data, gzip)
|
||||
|
||||
arr := []byte(src)
|
||||
data, _ := gcompress.Gzip(arr)
|
||||
gtest.Assert(data, gzip)
|
||||
data, _ = gcompress.UnGzip(gzip)
|
||||
gtest.Assert(data, arr)
|
||||
|
||||
data, _ = gcompress.UnGzip(gzip)
|
||||
gtest.Assert(data, arr)
|
||||
|
||||
data, _ = gcompress.UnGzip(gzip[1:])
|
||||
gtest.Assert(data, nil)
|
||||
data, _ = gcompress.UnGzip(gzip[1:])
|
||||
gtest.Assert(data, nil)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Gzip_UnGzip_File(t *testing.T) {
|
||||
srcPath := gfile.Join(gdebug.CallerDirectory(), "testdata", "gzip", "file.txt")
|
||||
dstPath1 := gfile.Join(gfile.TempDir(), gtime.TimestampNanoStr(), "gzip.zip")
|
||||
dstPath2 := gfile.Join(gfile.TempDir(), gtime.TimestampNanoStr(), "file.txt")
|
||||
|
||||
// Compress.
|
||||
gtest.Case(t, func() {
|
||||
err := gcompress.GzipFile(srcPath, dstPath1, 9)
|
||||
gtest.Assert(err, nil)
|
||||
defer gfile.Remove(dstPath1)
|
||||
gtest.Assert(gfile.Exists(dstPath1), true)
|
||||
|
||||
// Decompress.
|
||||
err = gcompress.UnGzipFile(dstPath1, dstPath2)
|
||||
gtest.Assert(err, nil)
|
||||
defer gfile.Remove(dstPath2)
|
||||
gtest.Assert(gfile.Exists(dstPath2), true)
|
||||
|
||||
gtest.Assert(gfile.GetContents(srcPath), gfile.GetContents(dstPath2))
|
||||
})
|
||||
}
|
||||
|
||||
1
encoding/gcompress/testdata/gzip/file.txt
vendored
Normal file
1
encoding/gcompress/testdata/gzip/file.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
This is a test file for gzip compression.
|
||||
@ -312,7 +312,7 @@ func SelfDir() string {
|
||||
return filepath.Dir(SelfPath())
|
||||
}
|
||||
|
||||
// Basename returns the last element of path.
|
||||
// Basename returns the last element of path, which contains file extension.
|
||||
// Trailing path separators are removed before extracting the last element.
|
||||
// If the path is empty, Base returns ".".
|
||||
// If the path consists entirely of separators, Basename returns a single separator.
|
||||
@ -320,7 +320,7 @@ func Basename(path string) string {
|
||||
return filepath.Base(path)
|
||||
}
|
||||
|
||||
// Name returns the last element of path without extension.
|
||||
// Name returns the last element of path without file extension.
|
||||
func Name(path string) string {
|
||||
base := filepath.Base(path)
|
||||
if i := strings.LastIndexByte(base, '.'); i != -1 {
|
||||
|
||||
@ -25,5 +25,5 @@ func MTimeMillisecond(path string) int64 {
|
||||
if e != nil {
|
||||
return 0
|
||||
}
|
||||
return int64(s.ModTime().Nanosecond() / 1000000)
|
||||
return s.ModTime().UnixNano() / 1000000
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2017-2019 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright 2017-2020 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
@ -8,15 +8,11 @@
|
||||
package gfpool
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gogf/gf/os/gfile"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/container/gmap"
|
||||
"github.com/gogf/gf/container/gpool"
|
||||
"github.com/gogf/gf/container/gtype"
|
||||
"github.com/gogf/gf/os/gfsnotify"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// File pointer pool.
|
||||
@ -30,6 +26,7 @@ type Pool struct {
|
||||
// File is an item in the pool.
|
||||
type File struct {
|
||||
*os.File // Underlying file pointer.
|
||||
stat os.FileInfo // State of current file pointer.
|
||||
pid int // Belonging pool id, which is set when file pointer created. It's used to check whether the pool is recreated.
|
||||
pool *Pool // Belonging ool.
|
||||
flag int // Flash for opening file.
|
||||
@ -41,137 +38,3 @@ var (
|
||||
// Global file pointer pool.
|
||||
pools = gmap.NewStrAnyMap(true)
|
||||
)
|
||||
|
||||
// Open creates and returns a file item with given file path, flag and opening permission.
|
||||
// It automatically creates an associated file pointer pool internally when it's called first time.
|
||||
// It retrieves a file item from the file pointer pool after then.
|
||||
func Open(path string, flag int, perm os.FileMode, ttl ...time.Duration) (file *File, err error) {
|
||||
var fpTTL time.Duration
|
||||
if len(ttl) > 0 {
|
||||
fpTTL = ttl[0]
|
||||
}
|
||||
path, err = gfile.Search(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pool := pools.GetOrSetFuncLock(
|
||||
fmt.Sprintf("%s&%d&%d&%d", path, flag, fpTTL, perm),
|
||||
func() interface{} {
|
||||
return New(path, flag, perm, fpTTL)
|
||||
},
|
||||
).(*Pool)
|
||||
|
||||
return pool.File()
|
||||
}
|
||||
|
||||
// New creates and returns a file pointer pool with given file path, flag and opening permission.
|
||||
//
|
||||
// Note the expiration logic:
|
||||
// ttl = 0 : not expired;
|
||||
// ttl < 0 : immediate expired after use;
|
||||
// ttl > 0 : timeout expired;
|
||||
// It is not expired in default.
|
||||
func New(path string, flag int, perm os.FileMode, ttl ...time.Duration) *Pool {
|
||||
var fpTTL time.Duration
|
||||
if len(ttl) > 0 {
|
||||
fpTTL = ttl[0]
|
||||
}
|
||||
p := &Pool{
|
||||
id: gtype.NewInt(),
|
||||
ttl: fpTTL,
|
||||
init: gtype.NewBool(),
|
||||
}
|
||||
p.pool = newFilePool(p, path, flag, perm, fpTTL)
|
||||
return p
|
||||
}
|
||||
|
||||
// newFilePool creates and returns a file pointer pool with given file path, flag and opening permission.
|
||||
func newFilePool(p *Pool, path string, flag int, perm os.FileMode, ttl time.Duration) *gpool.Pool {
|
||||
pool := gpool.New(ttl, func() (interface{}, error) {
|
||||
file, err := os.OpenFile(path, flag, perm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &File{
|
||||
File: file,
|
||||
pool: p,
|
||||
pid: p.id.Val(),
|
||||
flag: flag,
|
||||
perm: perm,
|
||||
path: path,
|
||||
}, nil
|
||||
}, func(i interface{}) {
|
||||
_ = i.(*File).File.Close()
|
||||
})
|
||||
return pool
|
||||
}
|
||||
|
||||
// File retrieves file item from the file pointer pool and returns it. It creates one if
|
||||
// the file pointer pool is empty.
|
||||
// Note that it should be closed when it will never be used. When it's closed, it is not
|
||||
// really closed the underlying file pointer but put back to the file pinter pool.
|
||||
func (p *Pool) File() (*File, error) {
|
||||
if v, err := p.pool.Get(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
f := v.(*File)
|
||||
stat, err := os.Stat(f.path)
|
||||
if f.flag&os.O_CREATE > 0 {
|
||||
if os.IsNotExist(err) {
|
||||
if file, err := os.OpenFile(f.path, f.flag, f.perm); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
f.File = file
|
||||
if stat, err = f.Stat(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if f.flag&os.O_TRUNC > 0 {
|
||||
if stat.Size() > 0 {
|
||||
if err := f.Truncate(0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
if f.flag&os.O_APPEND > 0 {
|
||||
if _, err := f.Seek(0, 2); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if _, err := f.Seek(0, 0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// It firstly checks using !p.init.Val() for performance purpose.
|
||||
if !p.init.Val() && p.init.Cas(false, true) {
|
||||
_, _ = gfsnotify.Add(f.path, func(event *gfsnotify.Event) {
|
||||
// If teh file is removed or renamed, recreates the pool by increasing the pool id.
|
||||
if event.IsRemove() || event.IsRename() {
|
||||
// It drops the old pool.
|
||||
p.id.Add(1)
|
||||
// Clears the pool items staying in the pool.
|
||||
p.pool.Clear()
|
||||
// It uses another adding to drop the file items between the two adding.
|
||||
// Whenever the pool id changes, the pool will be recreated.
|
||||
p.id.Add(1)
|
||||
}
|
||||
}, false)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes current file pointer pool.
|
||||
func (p *Pool) Close() {
|
||||
p.pool.Close()
|
||||
}
|
||||
|
||||
// Close puts the file pointer back to the file pointer pool.
|
||||
func (f *File) Close() error {
|
||||
if f.pid == f.pool.id.Val() {
|
||||
return f.pool.pool.Put(f)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
55
os/gfpool/gfpool_file.go
Normal file
55
os/gfpool/gfpool_file.go
Normal file
@ -0,0 +1,55 @@
|
||||
// Copyright 2017-2020 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package gfpool
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Open creates and returns a file item with given file path, flag and opening permission.
|
||||
// It automatically creates an associated file pointer pool internally when it's called first time.
|
||||
// It retrieves a file item from the file pointer pool after then.
|
||||
func Open(path string, flag int, perm os.FileMode, ttl ...time.Duration) (file *File, err error) {
|
||||
var fpTTL time.Duration
|
||||
if len(ttl) > 0 {
|
||||
fpTTL = ttl[0]
|
||||
}
|
||||
// DO NOT search the path here wasting performance!
|
||||
// Leave following codes just for warning you.
|
||||
//
|
||||
//path, err = gfile.Search(path)
|
||||
//if err != nil {
|
||||
// return nil, err
|
||||
//}
|
||||
pool := pools.GetOrSetFuncLock(
|
||||
fmt.Sprintf("%s&%d&%d&%d", path, flag, fpTTL, perm),
|
||||
func() interface{} {
|
||||
return New(path, flag, perm, fpTTL)
|
||||
},
|
||||
).(*Pool)
|
||||
|
||||
return pool.File()
|
||||
}
|
||||
|
||||
// Stat returns the FileInfo structure describing file.
|
||||
func (f *File) Stat() (os.FileInfo, error) {
|
||||
if f.stat == nil {
|
||||
return nil, errors.New("file stat is empty")
|
||||
}
|
||||
return f.stat, nil
|
||||
}
|
||||
|
||||
// Close puts the file pointer back to the file pointer pool.
|
||||
func (f *File) Close() error {
|
||||
if f.pid == f.pool.id.Val() {
|
||||
return f.pool.pool.Put(f)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
121
os/gfpool/gfpool_pool.go
Normal file
121
os/gfpool/gfpool_pool.go
Normal file
@ -0,0 +1,121 @@
|
||||
// Copyright 2017-2020 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package gfpool
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/container/gpool"
|
||||
"github.com/gogf/gf/container/gtype"
|
||||
"github.com/gogf/gf/os/gfsnotify"
|
||||
)
|
||||
|
||||
// New creates and returns a file pointer pool with given file path, flag and opening permission.
|
||||
//
|
||||
// Note the expiration logic:
|
||||
// ttl = 0 : not expired;
|
||||
// ttl < 0 : immediate expired after use;
|
||||
// ttl > 0 : timeout expired;
|
||||
// It is not expired in default.
|
||||
func New(path string, flag int, perm os.FileMode, ttl ...time.Duration) *Pool {
|
||||
var fpTTL time.Duration
|
||||
if len(ttl) > 0 {
|
||||
fpTTL = ttl[0]
|
||||
}
|
||||
p := &Pool{
|
||||
id: gtype.NewInt(),
|
||||
ttl: fpTTL,
|
||||
init: gtype.NewBool(),
|
||||
}
|
||||
p.pool = newFilePool(p, path, flag, perm, fpTTL)
|
||||
return p
|
||||
}
|
||||
|
||||
// newFilePool creates and returns a file pointer pool with given file path, flag and opening permission.
|
||||
func newFilePool(p *Pool, path string, flag int, perm os.FileMode, ttl time.Duration) *gpool.Pool {
|
||||
pool := gpool.New(ttl, func() (interface{}, error) {
|
||||
file, err := os.OpenFile(path, flag, perm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &File{
|
||||
File: file,
|
||||
pid: p.id.Val(),
|
||||
pool: p,
|
||||
flag: flag,
|
||||
perm: perm,
|
||||
path: path,
|
||||
}, nil
|
||||
}, func(i interface{}) {
|
||||
_ = i.(*File).File.Close()
|
||||
})
|
||||
return pool
|
||||
}
|
||||
|
||||
// File retrieves file item from the file pointer pool and returns it. It creates one if
|
||||
// the file pointer pool is empty.
|
||||
// Note that it should be closed when it will never be used. When it's closed, it is not
|
||||
// really closed the underlying file pointer but put back to the file pinter pool.
|
||||
func (p *Pool) File() (*File, error) {
|
||||
if v, err := p.pool.Get(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
var err error
|
||||
f := v.(*File)
|
||||
f.stat, err = os.Stat(f.path)
|
||||
if f.flag&os.O_CREATE > 0 {
|
||||
if os.IsNotExist(err) {
|
||||
if f.File, err = os.OpenFile(f.path, f.flag, f.perm); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
// Retrieve the state of the new created file.
|
||||
if f.stat, err = f.File.Stat(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if f.flag&os.O_TRUNC > 0 {
|
||||
if f.stat.Size() > 0 {
|
||||
if err = f.Truncate(0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
if f.flag&os.O_APPEND > 0 {
|
||||
if _, err = f.Seek(0, 2); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if _, err = f.Seek(0, 0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// It firstly checks using !p.init.Val() for performance purpose.
|
||||
if !p.init.Val() && p.init.Cas(false, true) {
|
||||
_, _ = gfsnotify.Add(f.path, func(event *gfsnotify.Event) {
|
||||
// If teh file is removed or renamed, recreates the pool by increasing the pool id.
|
||||
if event.IsRemove() || event.IsRename() {
|
||||
// It drops the old pool.
|
||||
p.id.Add(1)
|
||||
// Clears the pool items staying in the pool.
|
||||
p.pool.Clear()
|
||||
// It uses another adding to drop the file items between the two adding.
|
||||
// Whenever the pool id changes, the pool will be recreated.
|
||||
p.id.Add(1)
|
||||
}
|
||||
}, false)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes current file pointer pool.
|
||||
func (p *Pool) Close() {
|
||||
p.pool.Close()
|
||||
}
|
||||
@ -5,44 +5,62 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Benchmark_os_Open_Close_ALLFlags(b *testing.B) {
|
||||
func Benchmark_OS_Open_Close_ALLFlags(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
f, _ := os.OpenFile("/tmp/bench-test", os.O_RDWR|os.O_CREATE|os.O_TRUNC|os.O_APPEND, 0666)
|
||||
f, err := os.OpenFile("/tmp/bench-test", os.O_RDWR|os.O_CREATE|os.O_TRUNC|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_gfpool_Open_Close_ALLFlags(b *testing.B) {
|
||||
func Benchmark_GFPool_Open_Close_ALLFlags(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
f, _ := Open("/tmp/bench-test", os.O_RDWR|os.O_CREATE|os.O_TRUNC|os.O_APPEND, 0666)
|
||||
f, err := Open("/tmp/bench-test", os.O_RDWR|os.O_CREATE|os.O_TRUNC|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_os_Open_Close_RDWR(b *testing.B) {
|
||||
func Benchmark_OS_Open_Close_RDWR(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
f, _ := os.OpenFile("/tmp/bench-test", os.O_RDWR, 0666)
|
||||
f, err := os.OpenFile("/tmp/bench-test", os.O_RDWR, 0666)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_gfpool_Open_Close_RDWR(b *testing.B) {
|
||||
func Benchmark_GFPool_Open_Close_RDWR(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
f, _ := Open("/tmp/bench-test", os.O_RDWR, 0666)
|
||||
f, err := Open("/tmp/bench-test", os.O_RDWR, 0666)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_os_Open_Close_RDONLY(b *testing.B) {
|
||||
func Benchmark_OS_Open_Close_RDONLY(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
f, _ := os.OpenFile("/tmp/bench-test", os.O_RDONLY, 0666)
|
||||
f, err := os.OpenFile("/tmp/bench-test", os.O_RDONLY, 0666)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_gfpool_Open_Close_RDONLY(b *testing.B) {
|
||||
func Benchmark_GFPool_Open_Close_RDONLY(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
f, _ := Open("/tmp/bench-test", os.O_RDONLY, 0666)
|
||||
f, err := Open("/tmp/bench-test", os.O_RDONLY, 0666)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,9 +10,11 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/internal/intlog"
|
||||
"github.com/gogf/gf/os/gtimer"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/debug/gdebug"
|
||||
@ -26,8 +28,9 @@ import (
|
||||
|
||||
// Logger is the struct for logging management.
|
||||
type Logger struct {
|
||||
parent *Logger // Parent logger.
|
||||
config Config // Logger configuration.
|
||||
mu sync.Mutex // Mutex is not for common logging, but for file rotation feature.
|
||||
parent *Logger // Parent logger.
|
||||
config Config // Logger configuration.
|
||||
}
|
||||
|
||||
const (
|
||||
@ -50,9 +53,11 @@ const (
|
||||
|
||||
// New creates and returns a custom logger.
|
||||
func New() *Logger {
|
||||
return &Logger{
|
||||
logger := &Logger{
|
||||
config: DefaultConfig(),
|
||||
}
|
||||
gtimer.AddOnce(time.Second, logger.rotateChecks)
|
||||
return logger
|
||||
}
|
||||
|
||||
// NewWithWriter creates and returns a custom logger with io.Writer.
|
||||
@ -63,6 +68,7 @@ func NewWithWriter(writer io.Writer) *Logger {
|
||||
}
|
||||
|
||||
// Clone returns a new logger, which is the clone the current logger.
|
||||
// It's commonly used for chaining operations.
|
||||
func (l *Logger) Clone() *Logger {
|
||||
logger := Logger{}
|
||||
logger = *l
|
||||
@ -74,10 +80,6 @@ func (l *Logger) Clone() *Logger {
|
||||
// It returns nil if file logging is disabled, or file opening fails.
|
||||
func (l *Logger) getFilePointer() *gfpool.File {
|
||||
if path := l.config.Path; path != "" {
|
||||
// Content containing "{}" in the file name is formatted using gtime.
|
||||
file, _ := gregex.ReplaceStringFunc(`{.+?}`, l.config.File, func(s string) string {
|
||||
return gtime.Now().Format(strings.Trim(s, "{}"))
|
||||
})
|
||||
// Create path if it does not exist.
|
||||
if !gfile.Exists(path) {
|
||||
if err := gfile.Mkdir(path); err != nil {
|
||||
@ -86,7 +88,7 @@ func (l *Logger) getFilePointer() *gfpool.File {
|
||||
}
|
||||
}
|
||||
if fp, err := gfpool.Open(
|
||||
path+gfile.Separator+file,
|
||||
l.getFilePath(),
|
||||
gDEFAULT_FILE_POOL_FLAGS,
|
||||
gDEFAULT_FPOOL_PERM,
|
||||
gDEFAULT_FPOOL_EXPIRE); err == nil {
|
||||
@ -98,6 +100,15 @@ func (l *Logger) getFilePointer() *gfpool.File {
|
||||
return nil
|
||||
}
|
||||
|
||||
// getFilePath returns the logging file path.
|
||||
func (l *Logger) getFilePath() string {
|
||||
// Content containing "{}" in the file name is formatted using gtime.
|
||||
file, _ := gregex.ReplaceStringFunc(`{.+?}`, l.config.File, func(s string) string {
|
||||
return gtime.Now().Format(strings.Trim(s, "{}"))
|
||||
})
|
||||
return gfile.Join(l.config.Path, file)
|
||||
}
|
||||
|
||||
// print prints <s> to defined writer, logging file or passed <std>.
|
||||
func (l *Logger) print(std io.Writer, lead string, value ...interface{}) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
@ -183,6 +194,18 @@ func (l *Logger) printToWriter(std io.Writer, buffer *bytes.Buffer) {
|
||||
if l.config.Writer == nil {
|
||||
if f := l.getFilePointer(); f != nil {
|
||||
defer f.Close()
|
||||
// Rotation file size checks.
|
||||
if l.config.RotateSize > 0 {
|
||||
state, err := f.Stat()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if state.Size() > l.config.RotateSize {
|
||||
l.rotateFile()
|
||||
l.printToWriter(std, buffer)
|
||||
return
|
||||
}
|
||||
}
|
||||
if _, err := io.WriteString(f, buffer.String()); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
}
|
||||
|
||||
@ -14,34 +14,41 @@ import (
|
||||
"github.com/gogf/gf/util/gutil"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Config is the configuration object for logger.
|
||||
type Config struct {
|
||||
Writer io.Writer // Customized io.Writer.
|
||||
Flags int // Extra flags for logging output features.
|
||||
Path string // Logging directory path.
|
||||
File string // Format for logging file.
|
||||
Level int // Output level.
|
||||
Prefix string // Prefix string for every logging content.
|
||||
StSkip int // Skip count for stack.
|
||||
StStatus int // Stack status(1: enabled - default; 0: disabled)
|
||||
StFilter string // Stack string filter.
|
||||
HeaderPrint bool `c:"header"` // Print header or not(true in default).
|
||||
StdoutPrint bool `c:"stdout"` // Output to stdout or not(true in default).
|
||||
LevelPrefixes map[int]string // Logging level to its prefix string mapping.
|
||||
Writer io.Writer // Customized io.Writer.
|
||||
Flags int // Extra flags for logging output features.
|
||||
Path string // Logging directory path.
|
||||
File string // Format for logging file.
|
||||
Level int // Output level.
|
||||
Prefix string // Prefix string for every logging content.
|
||||
StSkip int // Skip count for stack.
|
||||
StStatus int // Stack status(1: enabled - default; 0: disabled)
|
||||
StFilter string // Stack string filter.
|
||||
HeaderPrint bool `c:"header"` // Print header or not(true in default).
|
||||
StdoutPrint bool `c:"stdout"` // Output to stdout or not(true in default).
|
||||
LevelPrefixes map[int]string // Logging level to its prefix string mapping.
|
||||
RotateSize int64 // Enables the rotate feature by set the size > 0 in bytes.
|
||||
RotateBackups int // Max backups for rotated files, default is 0, means no backups.
|
||||
RotateExpire time.Duration // Max expire age for rotated files. It's 0 in default, means no expiration.
|
||||
RotateCompress int // Compress level for rotated files using gzip algorithm. It's 0 in default, means no compression.
|
||||
RotateInterval time.Duration // Asynchronizely checks the backups and expiration at intervals. It's 1 minute in default.
|
||||
}
|
||||
|
||||
// DefaultConfig returns the default configuration for logger.
|
||||
func DefaultConfig() Config {
|
||||
c := Config{
|
||||
File: gDEFAULT_FILE_FORMAT,
|
||||
Flags: F_TIME_STD,
|
||||
Level: LEVEL_ALL,
|
||||
StStatus: 1,
|
||||
HeaderPrint: true,
|
||||
StdoutPrint: true,
|
||||
LevelPrefixes: make(map[int]string, len(defaultLevelPrefixes)),
|
||||
File: gDEFAULT_FILE_FORMAT,
|
||||
Flags: F_TIME_STD,
|
||||
Level: LEVEL_ALL,
|
||||
StStatus: 1,
|
||||
HeaderPrint: true,
|
||||
StdoutPrint: true,
|
||||
LevelPrefixes: make(map[int]string, len(defaultLevelPrefixes)),
|
||||
RotateInterval: time.Minute,
|
||||
}
|
||||
for k, v := range defaultLevelPrefixes {
|
||||
c.LevelPrefixes[k] = v
|
||||
|
||||
168
os/glog/glog_logger_rotate.go
Normal file
168
os/glog/glog_logger_rotate.go
Normal file
@ -0,0 +1,168 @@
|
||||
// Copyright 2020 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package glog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gogf/gf/container/garray"
|
||||
"github.com/gogf/gf/encoding/gcompress"
|
||||
"github.com/gogf/gf/internal/intlog"
|
||||
"github.com/gogf/gf/os/gfile"
|
||||
"github.com/gogf/gf/os/gtime"
|
||||
"github.com/gogf/gf/os/gtimer"
|
||||
"github.com/gogf/gf/text/gregex"
|
||||
)
|
||||
|
||||
// rotateFile rotates the current logging file.
|
||||
func (l *Logger) rotateFile() {
|
||||
// Rotation feature is not enabled as rotation file size is zero.
|
||||
if l.config.RotateSize == 0 {
|
||||
return
|
||||
}
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
filePath := l.getFilePath()
|
||||
// No backups, it then just removes the current logging file.
|
||||
if l.config.RotateBackups == 0 {
|
||||
if err := gfile.Remove(filePath); err != nil {
|
||||
intlog.Print(err)
|
||||
}
|
||||
intlog.Printf(`%d size exceeds, no backups set, remove original logging file: %s`, l.config.RotateSize, filePath)
|
||||
return
|
||||
}
|
||||
// Else it creates new backup files.
|
||||
var (
|
||||
dirPath = gfile.Dir(filePath)
|
||||
fileName = gfile.Name(filePath)
|
||||
fileExt = gfile.Ext(filePath)
|
||||
newFilePath = ""
|
||||
)
|
||||
for {
|
||||
// Rename the logging file by adding extra time information to milliseconds, like:
|
||||
// access -> access.20200102190000899
|
||||
// access.log -> access.20200102190000899.log
|
||||
// access.20200102.log -> access.20200102.20200102190000899.log
|
||||
newFilePath = gfile.Join(
|
||||
dirPath,
|
||||
fmt.Sprintf(`%s.%s%s`, fileName, gtime.Now().Format("YmdHisu"), fileExt),
|
||||
)
|
||||
if !gfile.Exists(newFilePath) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := gfile.Rename(filePath, newFilePath); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// rotateChecks timely checks the backups expiration and the compression.
|
||||
func (l *Logger) rotateChecks() {
|
||||
defer func() {
|
||||
gtimer.AddOnce(l.config.RotateInterval, l.rotateChecks)
|
||||
}()
|
||||
|
||||
// Checks whether file rotation not enabled.
|
||||
if l.config.RotateSize == 0 || l.config.RotateBackups == 0 {
|
||||
return
|
||||
}
|
||||
files, _ := gfile.ScanDirFile(l.config.Path, "*.*", true)
|
||||
intlog.Printf("logging rotation start checks: %+v", files)
|
||||
// Compression.
|
||||
needCompressFileArray := garray.NewStrArray()
|
||||
if l.config.RotateCompress > 0 {
|
||||
for _, file := range files {
|
||||
// Eg: access.20200102190000899.gz
|
||||
if gfile.ExtName(file) == "gz" {
|
||||
continue
|
||||
}
|
||||
// Eg:
|
||||
// access.20200102190000899
|
||||
// access.20200102190000899.log
|
||||
if gregex.IsMatchString(`.+\.\d{14,}`, file) {
|
||||
needCompressFileArray.Append(file)
|
||||
}
|
||||
}
|
||||
if needCompressFileArray.Len() > 0 {
|
||||
needCompressFileArray.Iterator(func(_ int, path string) bool {
|
||||
err := gcompress.GzipFile(path, path+".gz")
|
||||
if err == nil {
|
||||
intlog.Printf(`compressed done, remove original logging file: %s`, path)
|
||||
if err = gfile.Remove(path); err != nil {
|
||||
intlog.Print(err)
|
||||
}
|
||||
} else {
|
||||
intlog.Print(err)
|
||||
}
|
||||
return true
|
||||
})
|
||||
// Update the files array.
|
||||
files, _ = gfile.ScanDirFile(l.config.Path, "*.*", true)
|
||||
}
|
||||
}
|
||||
// Backups count limit and expiration checks.
|
||||
var (
|
||||
backupFilesMap = make(map[string]*garray.SortedArray)
|
||||
originalLoggingFilePath = ""
|
||||
)
|
||||
if l.config.RotateBackups > 0 || l.config.RotateExpire > 0 {
|
||||
for _, file := range files {
|
||||
originalLoggingFilePath, _ = gregex.ReplaceString(`\.\d{14,}`, "", file)
|
||||
if backupFilesMap[originalLoggingFilePath] == nil {
|
||||
backupFilesMap[originalLoggingFilePath] = garray.NewSortedArray(func(a, b interface{}) int {
|
||||
// Sorted by backup file mtime.
|
||||
// The old backup file is put in the head of array.
|
||||
file1 := a.(string)
|
||||
file2 := b.(string)
|
||||
result := gfile.MTimeMillisecond(file1) - gfile.MTimeMillisecond(file2)
|
||||
if result <= 0 {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
})
|
||||
}
|
||||
if gregex.IsMatchString(`.+\.\d{14,}`, file) {
|
||||
backupFilesMap[originalLoggingFilePath].Add(file)
|
||||
}
|
||||
}
|
||||
intlog.Printf(`calculated backup files map: %+v`, backupFilesMap)
|
||||
for _, array := range backupFilesMap {
|
||||
for i := 0; i < array.Len()-l.config.RotateBackups; i++ {
|
||||
path := array.PopLeft().(string)
|
||||
intlog.Printf(`remove exceeded backup file: %s`, path)
|
||||
if err := gfile.Remove(path); err != nil {
|
||||
intlog.Print(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Expiration checks.
|
||||
if l.config.RotateExpire > 0 {
|
||||
nowTimestampMilli := gtime.TimestampMilli()
|
||||
expireMillisecond := l.config.RotateExpire.Milliseconds()
|
||||
for _, array := range backupFilesMap {
|
||||
array.Iterator(func(_ int, v interface{}) bool {
|
||||
path := v.(string)
|
||||
mtime := gfile.MTimeMillisecond(path)
|
||||
differ := nowTimestampMilli - mtime
|
||||
if differ > expireMillisecond {
|
||||
intlog.Printf(
|
||||
`%d - %d = %d > %d, remove expired backup file: %s`,
|
||||
nowTimestampMilli, mtime, differ,
|
||||
expireMillisecond,
|
||||
path,
|
||||
)
|
||||
if err := gfile.Remove(path); err != nil {
|
||||
intlog.Print(err)
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
56
os/glog/glog_z_unit_rotate_test.go
Normal file
56
os/glog/glog_z_unit_rotate_test.go
Normal file
@ -0,0 +1,56 @@
|
||||
// Copyright 2020 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package glog_test
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/frame/g"
|
||||
"github.com/gogf/gf/os/gfile"
|
||||
"github.com/gogf/gf/os/glog"
|
||||
"github.com/gogf/gf/os/gtime"
|
||||
"github.com/gogf/gf/test/gtest"
|
||||
"github.com/gogf/gf/text/gstr"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Test_Rotate(t *testing.T) {
|
||||
gtest.Case(t, func() {
|
||||
l := glog.New()
|
||||
p := gfile.Join(gfile.TempDir(), gtime.TimestampNanoStr())
|
||||
err := l.SetConfigWithMap(g.Map{
|
||||
"Path": p,
|
||||
"File": "access.log",
|
||||
"StdoutPrint": false,
|
||||
"RotateSize": 10,
|
||||
"RotateBackups": 2,
|
||||
"RotateExpire": 5 * time.Second,
|
||||
"RotateCompress": 9,
|
||||
"RotateInterval": time.Second, // For unit testing only.
|
||||
})
|
||||
gtest.Assert(err, nil)
|
||||
defer gfile.Remove(p)
|
||||
|
||||
s := "1234567890abcdefg"
|
||||
for i := 0; i < 10; i++ {
|
||||
l.Print(s)
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 3)
|
||||
|
||||
files, err := gfile.ScanDirFile(p, "*.gz")
|
||||
gtest.Assert(err, nil)
|
||||
gtest.Assert(len(files), 2)
|
||||
|
||||
content := gfile.GetContents(gfile.Join(p, "access.log"))
|
||||
gtest.Assert(gstr.Count(content, s), 1)
|
||||
|
||||
time.Sleep(time.Second * 4)
|
||||
files, err = gfile.ScanDirFile(p, "*.gz")
|
||||
gtest.Assert(err, nil)
|
||||
gtest.Assert(len(files), 0)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user