Skip to content

Commit

Permalink
Add log file writer.
Browse files Browse the repository at this point in the history
  • Loading branch information
edoger committed May 3, 2021
1 parent 4997794 commit 4beda40
Show file tree
Hide file tree
Showing 2 changed files with 211 additions and 0 deletions.
122 changes: 122 additions & 0 deletions file_writer.go
@@ -0,0 +1,122 @@
// Copyright 2021 The ZKits Project Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package logger

import (
"io"
"os"
"path/filepath"
"sync"
"sync/atomic"
"time"

"github.com/edoger/zkits-logger/internal"
)

// NewFileWriter creates and returns an io.WriteCloser instance from the given path.
// The max parameter is used to limit the maximum size of the log file, if it is 0, the
// file size limit will be disabled. The log files that exceed the maximum size limit
// will be renamed.
func NewFileWriter(name string, max uint32) (io.WriteCloser, error) {
if abs, err := filepath.Abs(name); err != nil {
return nil, err
} else {
name = abs
}
// Before opening the log file, make sure that the directory must exist.
if err := os.MkdirAll(filepath.Dir(name), 0766); err != nil {
return nil, err
}
fd, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
return nil, err
}
i, err := fd.Stat()
if err != nil {
_ = fd.Close()
return nil, err
}
return &fileWriter{name: name, max: max, size: uint32(i.Size()), fd: fd}, nil
}

// MustNewFileWriter is like NewFileWriter, but triggers a panic when an error occurs.
func MustNewFileWriter(name string, max uint32) io.WriteCloser {
w, err := NewFileWriter(name, max)
if err != nil {
panic(err)
}
return w
}

// The built-in log file writer.
type fileWriter struct {
name string
mu sync.RWMutex
max uint32
size uint32
fd *os.File
}

// Write is an implementation of the io.WriteCloser interface, used to write a single
// log data to a file.
func (w *fileWriter) Write(b []byte) (n int, err error) {
w.mu.RLock()
fd := w.fd
w.mu.RUnlock()
n, err = fd.Write(b)
if w.max != 0 && atomic.AddUint32(&w.size, uint32(n)) >= w.max {
w.swap()
}
return
}

// Switch to the new log file.
func (w *fileWriter) swap() {
w.mu.Lock()
defer w.mu.Unlock()
if atomic.LoadUint32(&w.size) < w.max {
return
}
// We use a second-level date as the suffix name of the archive log,
// which may change in the future.
suffix := time.Now().Format("20060102150405")
if err := os.Rename(w.name, w.name+"."+suffix); err != nil {
internal.EchoError("Failed to rename log file %s to %s.%s: %s.", w.name, w.name, suffix, err)
return
}
fd, err := os.OpenFile(w.name, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
internal.EchoError("Failed to open log file %s: %s.", w.name, err)
return
}
old := w.fd
w.fd = fd
atomic.StoreUint32(&w.size, 0)

if err = old.Sync(); err != nil {
internal.EchoError("Failed to sync log file %s.%s: %s.", w.name, suffix, err)
}
if err = old.Close(); err != nil {
internal.EchoError("Failed to close log file %s.%s: %s.", w.name, suffix, err)
}
}

// Close is an implementation of the io.WriteCloser interface.
func (w *fileWriter) Close() (err error) {
w.mu.RLock()
err = w.fd.Close()
w.mu.RUnlock()
return
}
89 changes: 89 additions & 0 deletions file_writer_test.go
@@ -0,0 +1,89 @@
// Copyright 2021 The ZKits Project Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package logger

import (
"io/ioutil"
"path/filepath"
"strings"
"testing"
)

func TestNewFileWriter(t *testing.T) {
dir := t.TempDir()
name := filepath.Join(dir, "test.log")

if w, err := NewFileWriter(name, 1024); err != nil {
t.Fatal(err)
} else {
if w == nil {
t.Fatal("NewFileWriter(): nil")
}
}

if _, err := NewFileWriter(dir, 1024); err == nil {
t.Fatal("NewFileWriter(): nil error")
}
}

func TestMustNewFileWriter(t *testing.T) {
dir := t.TempDir()
name := filepath.Join(dir, "test.log")

if w := MustNewFileWriter(name, 1024); w == nil {
t.Fatal("MustNewFileWriter(): nil")
}

defer func() {
if v := recover(); v == nil {
t.Fatal("MustNewFileWriter(): not panic")
}
}()
MustNewFileWriter(dir, 1024)
}

func TestFileWriter(t *testing.T) {
dir := t.TempDir()
name := filepath.Join(dir, "test.log")
w := MustNewFileWriter(name, 1024)
defer func() {
if err := w.Close(); err != nil {
t.Fatal(err)
}
}()

data := strings.Repeat("1", 1024)
if n, err := w.Write([]byte(data)); err != nil {
t.Fatal(err)
} else {
if n != 1024 {
t.Fatalf("FileWriter.Write(): %d", n)
}
}
if matches, err := filepath.Glob(name + ".*"); err != nil {
t.Fatal(err)
} else {
if len(matches) == 0 {
t.Fatal("FileWriter.Write(): not rename")
}
got, err := ioutil.ReadFile(matches[0])
if err != nil {
t.Fatal(err)
}
if string(got) != data {
t.Fatalf("FileWriter.Write(): got %s", string(got))
}
}
}

0 comments on commit 4beda40

Please sign in to comment.