重构代码
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
package archivex
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"b612.me/starlog/internal/runtimex"
|
||||
)
|
||||
|
||||
type FileRecord struct {
|
||||
FullPath string
|
||||
Pointer *os.File
|
||||
}
|
||||
|
||||
type Runner struct {
|
||||
Cancel context.CancelFunc
|
||||
Done chan struct{}
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
files runtimex.MapKV
|
||||
runners runtimex.MapKV
|
||||
}
|
||||
|
||||
func NewStore() *Store {
|
||||
return &Store{
|
||||
files: runtimex.NewMapKV(),
|
||||
runners: runtimex.NewMapKV(),
|
||||
}
|
||||
}
|
||||
|
||||
func (store *Store) SetFile(id string, record FileRecord) error {
|
||||
if store == nil {
|
||||
return nil
|
||||
}
|
||||
return store.files.Store(id, record)
|
||||
}
|
||||
|
||||
func (store *Store) GetFile(id string) (FileRecord, bool) {
|
||||
if store == nil {
|
||||
return FileRecord{}, false
|
||||
}
|
||||
val := store.files.MustGet(id)
|
||||
if val == nil {
|
||||
return FileRecord{}, false
|
||||
}
|
||||
record, ok := val.(FileRecord)
|
||||
if !ok {
|
||||
return FileRecord{}, false
|
||||
}
|
||||
return record, true
|
||||
}
|
||||
|
||||
func (store *Store) DeleteFile(id string) error {
|
||||
if store == nil {
|
||||
return nil
|
||||
}
|
||||
return store.files.Delete(id)
|
||||
}
|
||||
|
||||
func (store *Store) SetRunner(id string, runner *Runner) error {
|
||||
if store == nil {
|
||||
return nil
|
||||
}
|
||||
return store.runners.Store(id, runner)
|
||||
}
|
||||
|
||||
func (store *Store) GetRunner(id string) (*Runner, bool) {
|
||||
if store == nil {
|
||||
return nil, false
|
||||
}
|
||||
val := store.runners.MustGet(id)
|
||||
if val == nil {
|
||||
return nil, false
|
||||
}
|
||||
runner, ok := val.(*Runner)
|
||||
if !ok || runner == nil {
|
||||
return nil, false
|
||||
}
|
||||
return runner, true
|
||||
}
|
||||
|
||||
func (store *Store) DeleteRunner(id string) error {
|
||||
if store == nil {
|
||||
return nil
|
||||
}
|
||||
return store.runners.Delete(id)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package archivex
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStoreFileLifecycle(t *testing.T) {
|
||||
store := NewStore()
|
||||
tmp, err := ioutil.TempFile("", "starlog-archivex-*.log")
|
||||
if err != nil {
|
||||
t.Fatalf("TempFile failed: %v", err)
|
||||
}
|
||||
defer os.Remove(tmp.Name())
|
||||
defer tmp.Close()
|
||||
|
||||
rec := FileRecord{FullPath: tmp.Name(), Pointer: tmp}
|
||||
if err := store.SetFile("id-1", rec); err != nil {
|
||||
t.Fatalf("SetFile failed: %v", err)
|
||||
}
|
||||
got, ok := store.GetFile("id-1")
|
||||
if !ok {
|
||||
t.Fatalf("GetFile should return stored record")
|
||||
}
|
||||
if got.FullPath != rec.FullPath || got.Pointer != rec.Pointer {
|
||||
t.Fatalf("unexpected file record: %+v", got)
|
||||
}
|
||||
if err := store.DeleteFile("id-1"); err != nil {
|
||||
t.Fatalf("DeleteFile failed: %v", err)
|
||||
}
|
||||
if _, ok := store.GetFile("id-1"); ok {
|
||||
t.Fatalf("record should not exist after DeleteFile")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreRunnerLifecycle(t *testing.T) {
|
||||
store := NewStore()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
runner := &Runner{Cancel: cancel, Done: make(chan struct{})}
|
||||
|
||||
if err := store.SetRunner("r-1", runner); err != nil {
|
||||
t.Fatalf("SetRunner failed: %v", err)
|
||||
}
|
||||
got, ok := store.GetRunner("r-1")
|
||||
if !ok || got != runner {
|
||||
t.Fatalf("GetRunner should return stored runner")
|
||||
}
|
||||
if err := store.DeleteRunner("r-1"); err != nil {
|
||||
t.Fatalf("DeleteRunner failed: %v", err)
|
||||
}
|
||||
if _, ok := store.GetRunner("r-1"); ok {
|
||||
t.Fatalf("runner should not exist after DeleteRunner")
|
||||
}
|
||||
_ = ctx
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package fsutil
|
||||
|
||||
import "os"
|
||||
|
||||
func Exists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func IsFile(path string) bool {
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return !stat.IsDir()
|
||||
}
|
||||
|
||||
func IsFolder(path string) bool {
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return stat.IsDir()
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package fsutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
func timespecToTime(ts syscall.Timespec) time.Time {
|
||||
return time.Unix(int64(ts.Sec), int64(ts.Nsec))
|
||||
}
|
||||
|
||||
func GetFileCreationTime(fileinfo os.FileInfo) time.Time {
|
||||
return timespecToTime(fileinfo.Sys().(*syscall.Stat_t).Ctimespec)
|
||||
}
|
||||
|
||||
func GetFileAccessTime(fileinfo os.FileInfo) time.Time {
|
||||
return timespecToTime(fileinfo.Sys().(*syscall.Stat_t).Atimespec)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package fsutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
func timespecToTime(ts syscall.Timespec) time.Time {
|
||||
return time.Unix(int64(ts.Sec), int64(ts.Nsec))
|
||||
}
|
||||
|
||||
func GetFileCreationTime(fileinfo os.FileInfo) time.Time {
|
||||
return timespecToTime(fileinfo.Sys().(*syscall.Stat_t).Ctim)
|
||||
}
|
||||
|
||||
func GetFileAccessTime(fileinfo os.FileInfo) time.Time {
|
||||
return timespecToTime(fileinfo.Sys().(*syscall.Stat_t).Atim)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package fsutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GetFileCreationTime(fileinfo os.FileInfo) time.Time {
|
||||
data := fileinfo.Sys().(*syscall.Win32FileAttributeData)
|
||||
return time.Unix(0, data.CreationTime.Nanoseconds())
|
||||
}
|
||||
|
||||
func GetFileAccessTime(fileinfo os.FileInfo) time.Time {
|
||||
data := fileinfo.Sys().(*syscall.Win32FileAttributeData)
|
||||
return time.Unix(0, data.LastAccessTime.Nanoseconds())
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
package multisinkx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type State string
|
||||
|
||||
const (
|
||||
StateHealthy State = "healthy"
|
||||
StateDegraded State = "degraded"
|
||||
StateRecovered State = "recovered"
|
||||
)
|
||||
|
||||
const (
|
||||
stateHealthy uint32 = iota
|
||||
stateDegraded
|
||||
stateRecovered
|
||||
)
|
||||
|
||||
type Stats struct {
|
||||
Index int
|
||||
Writes uint64
|
||||
WriteErrors uint64
|
||||
Closes uint64
|
||||
CloseErrors uint64
|
||||
ConsecutiveWriteErrors uint64
|
||||
ConsecutiveCloseErrors uint64
|
||||
LastWriteError string
|
||||
LastCloseError string
|
||||
State State
|
||||
}
|
||||
|
||||
type Snapshot struct {
|
||||
ContinueOnError bool
|
||||
Sinks []Stats
|
||||
}
|
||||
|
||||
type Sink interface {
|
||||
Write([]byte) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
type slot struct {
|
||||
sink Sink
|
||||
writeCount uint64
|
||||
writeErrorCount uint64
|
||||
closeCount uint64
|
||||
closeErrorCount uint64
|
||||
consecutiveWriteErrors uint64
|
||||
consecutiveCloseErrors uint64
|
||||
state uint32
|
||||
mu sync.RWMutex
|
||||
lastWriteError string
|
||||
lastCloseError string
|
||||
}
|
||||
|
||||
func newSlot(sink Sink) *slot {
|
||||
result := &slot{
|
||||
sink: sink,
|
||||
}
|
||||
atomic.StoreUint32(&result.state, stateHealthy)
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *slot) setLastWriteError(err error) {
|
||||
msg := ""
|
||||
if err != nil {
|
||||
msg = err.Error()
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.lastWriteError = msg
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *slot) setLastCloseError(err error) {
|
||||
msg := ""
|
||||
if err != nil {
|
||||
msg = err.Error()
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.lastCloseError = msg
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *slot) setStateHealthyOrRecovered() {
|
||||
if atomic.LoadUint64(&s.writeErrorCount)+atomic.LoadUint64(&s.closeErrorCount) > 0 {
|
||||
atomic.StoreUint32(&s.state, stateRecovered)
|
||||
return
|
||||
}
|
||||
atomic.StoreUint32(&s.state, stateHealthy)
|
||||
}
|
||||
|
||||
func (s *slot) observeWrite(err error) {
|
||||
atomic.AddUint64(&s.writeCount, 1)
|
||||
if err == nil {
|
||||
atomic.StoreUint64(&s.consecutiveWriteErrors, 0)
|
||||
s.setLastWriteError(nil)
|
||||
s.setStateHealthyOrRecovered()
|
||||
return
|
||||
}
|
||||
atomic.AddUint64(&s.writeErrorCount, 1)
|
||||
atomic.AddUint64(&s.consecutiveWriteErrors, 1)
|
||||
atomic.StoreUint32(&s.state, stateDegraded)
|
||||
s.setLastWriteError(err)
|
||||
}
|
||||
|
||||
func (s *slot) observeClose(err error) {
|
||||
atomic.AddUint64(&s.closeCount, 1)
|
||||
if err == nil {
|
||||
atomic.StoreUint64(&s.consecutiveCloseErrors, 0)
|
||||
s.setLastCloseError(nil)
|
||||
s.setStateHealthyOrRecovered()
|
||||
return
|
||||
}
|
||||
atomic.AddUint64(&s.closeErrorCount, 1)
|
||||
atomic.AddUint64(&s.consecutiveCloseErrors, 1)
|
||||
atomic.StoreUint32(&s.state, stateDegraded)
|
||||
s.setLastCloseError(err)
|
||||
}
|
||||
|
||||
func (s *slot) snapshot(index int) Stats {
|
||||
lastWriteErr := ""
|
||||
lastCloseErr := ""
|
||||
s.mu.RLock()
|
||||
lastWriteErr = s.lastWriteError
|
||||
lastCloseErr = s.lastCloseError
|
||||
s.mu.RUnlock()
|
||||
return Stats{
|
||||
Index: index,
|
||||
Writes: atomic.LoadUint64(&s.writeCount),
|
||||
WriteErrors: atomic.LoadUint64(&s.writeErrorCount),
|
||||
Closes: atomic.LoadUint64(&s.closeCount),
|
||||
CloseErrors: atomic.LoadUint64(&s.closeErrorCount),
|
||||
ConsecutiveWriteErrors: atomic.LoadUint64(&s.consecutiveWriteErrors),
|
||||
ConsecutiveCloseErrors: atomic.LoadUint64(&s.consecutiveCloseErrors),
|
||||
LastWriteError: lastWriteErr,
|
||||
LastCloseError: lastCloseErr,
|
||||
State: decodeState(atomic.LoadUint32(&s.state)),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *slot) resetStats() {
|
||||
atomic.StoreUint64(&s.writeCount, 0)
|
||||
atomic.StoreUint64(&s.writeErrorCount, 0)
|
||||
atomic.StoreUint64(&s.closeCount, 0)
|
||||
atomic.StoreUint64(&s.closeErrorCount, 0)
|
||||
atomic.StoreUint64(&s.consecutiveWriteErrors, 0)
|
||||
atomic.StoreUint64(&s.consecutiveCloseErrors, 0)
|
||||
atomic.StoreUint32(&s.state, stateHealthy)
|
||||
s.mu.Lock()
|
||||
s.lastWriteError = ""
|
||||
s.lastCloseError = ""
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func decodeState(state uint32) State {
|
||||
switch state {
|
||||
case stateDegraded:
|
||||
return StateDegraded
|
||||
case stateRecovered:
|
||||
return StateRecovered
|
||||
default:
|
||||
return StateHealthy
|
||||
}
|
||||
}
|
||||
|
||||
type MultiSink struct {
|
||||
mu sync.RWMutex
|
||||
slots []*slot
|
||||
continueOnError bool
|
||||
}
|
||||
|
||||
func New(sinks ...Sink) *MultiSink {
|
||||
multi := &MultiSink{
|
||||
continueOnError: true,
|
||||
slots: make([]*slot, 0, len(sinks)),
|
||||
}
|
||||
multi.SetSinks(sinks...)
|
||||
return multi
|
||||
}
|
||||
|
||||
func (sink *MultiSink) SetSinks(sinks ...Sink) {
|
||||
if sink == nil {
|
||||
return
|
||||
}
|
||||
filtered := make([]Sink, 0, len(sinks))
|
||||
for _, item := range sinks {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, item)
|
||||
}
|
||||
slots := make([]*slot, 0, len(filtered))
|
||||
for _, item := range filtered {
|
||||
slots = append(slots, newSlot(item))
|
||||
}
|
||||
sink.mu.Lock()
|
||||
sink.slots = slots
|
||||
sink.mu.Unlock()
|
||||
}
|
||||
|
||||
func (sink *MultiSink) AddSink(item Sink) {
|
||||
if sink == nil || item == nil {
|
||||
return
|
||||
}
|
||||
sink.mu.Lock()
|
||||
sink.slots = append(sink.slots, newSlot(item))
|
||||
sink.mu.Unlock()
|
||||
}
|
||||
|
||||
func (sink *MultiSink) SetContinueOnError(continueOnError bool) {
|
||||
if sink == nil {
|
||||
return
|
||||
}
|
||||
sink.mu.Lock()
|
||||
sink.continueOnError = continueOnError
|
||||
sink.mu.Unlock()
|
||||
}
|
||||
|
||||
func (sink *MultiSink) ContinueOnError() bool {
|
||||
if sink == nil {
|
||||
return true
|
||||
}
|
||||
sink.mu.RLock()
|
||||
defer sink.mu.RUnlock()
|
||||
return sink.continueOnError
|
||||
}
|
||||
|
||||
func (sink *MultiSink) SinkCount() int {
|
||||
if sink == nil {
|
||||
return 0
|
||||
}
|
||||
sink.mu.RLock()
|
||||
defer sink.mu.RUnlock()
|
||||
return len(sink.slots)
|
||||
}
|
||||
|
||||
func (sink *MultiSink) GetStats() Snapshot {
|
||||
if sink == nil {
|
||||
return Snapshot{
|
||||
ContinueOnError: true,
|
||||
Sinks: nil,
|
||||
}
|
||||
}
|
||||
current, continueOnError := sink.snapshot()
|
||||
stats := make([]Stats, 0, len(current))
|
||||
for index, item := range current {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
stats = append(stats, item.snapshot(index))
|
||||
}
|
||||
return Snapshot{
|
||||
ContinueOnError: continueOnError,
|
||||
Sinks: stats,
|
||||
}
|
||||
}
|
||||
|
||||
func (sink *MultiSink) ResetStats() {
|
||||
if sink == nil {
|
||||
return
|
||||
}
|
||||
current, _ := sink.snapshot()
|
||||
for _, item := range current {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
item.resetStats()
|
||||
}
|
||||
}
|
||||
|
||||
func (sink *MultiSink) Write(data []byte) error {
|
||||
if sink == nil {
|
||||
return nil
|
||||
}
|
||||
current, continueOnError := sink.snapshot()
|
||||
if len(current) == 0 {
|
||||
return nil
|
||||
}
|
||||
var errs []error
|
||||
for _, item := range current {
|
||||
if item == nil || item.sink == nil {
|
||||
continue
|
||||
}
|
||||
err := item.sink.Write(data)
|
||||
item.observeWrite(err)
|
||||
if err != nil {
|
||||
if !continueOnError {
|
||||
return err
|
||||
}
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return packErrors("write", errs)
|
||||
}
|
||||
|
||||
func (sink *MultiSink) Close() error {
|
||||
if sink == nil {
|
||||
return nil
|
||||
}
|
||||
current, continueOnError := sink.snapshot()
|
||||
var errs []error
|
||||
for _, item := range current {
|
||||
if item == nil || item.sink == nil {
|
||||
continue
|
||||
}
|
||||
err := item.sink.Close()
|
||||
item.observeClose(err)
|
||||
if err != nil {
|
||||
if !continueOnError {
|
||||
return err
|
||||
}
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return packErrors("close", errs)
|
||||
}
|
||||
|
||||
func (sink *MultiSink) snapshot() ([]*slot, bool) {
|
||||
sink.mu.RLock()
|
||||
defer sink.mu.RUnlock()
|
||||
current := make([]*slot, len(sink.slots))
|
||||
copy(current, sink.slots)
|
||||
return current, sink.continueOnError
|
||||
}
|
||||
|
||||
func packErrors(action string, errs []error) error {
|
||||
if len(errs) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(errs) == 1 {
|
||||
return errs[0]
|
||||
}
|
||||
return fmt.Errorf("multi sink %s failed with %d errors: %v", action, len(errs), errs[0])
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package observerx
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type Buffer struct {
|
||||
mu sync.RWMutex
|
||||
items []interface{}
|
||||
limit int
|
||||
dropped uint64
|
||||
}
|
||||
|
||||
func NewBuffer() *Buffer {
|
||||
return &Buffer{
|
||||
items: make([]interface{}, 0, 16),
|
||||
limit: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (buffer *Buffer) Add(item interface{}) {
|
||||
if buffer == nil {
|
||||
return
|
||||
}
|
||||
buffer.mu.Lock()
|
||||
if buffer.limit > 0 && len(buffer.items) >= buffer.limit {
|
||||
buffer.items = buffer.items[1:]
|
||||
atomic.AddUint64(&buffer.dropped, 1)
|
||||
}
|
||||
buffer.items = append(buffer.items, item)
|
||||
buffer.mu.Unlock()
|
||||
}
|
||||
|
||||
func (buffer *Buffer) SetLimit(limit int) {
|
||||
if buffer == nil {
|
||||
return
|
||||
}
|
||||
if limit < 0 {
|
||||
limit = 0
|
||||
}
|
||||
buffer.mu.Lock()
|
||||
buffer.limit = limit
|
||||
if limit > 0 && len(buffer.items) > limit {
|
||||
dropped := len(buffer.items) - limit
|
||||
buffer.items = buffer.items[dropped:]
|
||||
atomic.AddUint64(&buffer.dropped, uint64(dropped))
|
||||
}
|
||||
buffer.mu.Unlock()
|
||||
}
|
||||
|
||||
func (buffer *Buffer) Limit() int {
|
||||
if buffer == nil {
|
||||
return 0
|
||||
}
|
||||
buffer.mu.RLock()
|
||||
defer buffer.mu.RUnlock()
|
||||
return buffer.limit
|
||||
}
|
||||
|
||||
func (buffer *Buffer) Count() int {
|
||||
if buffer == nil {
|
||||
return 0
|
||||
}
|
||||
buffer.mu.RLock()
|
||||
defer buffer.mu.RUnlock()
|
||||
return len(buffer.items)
|
||||
}
|
||||
|
||||
func (buffer *Buffer) Dropped() uint64 {
|
||||
if buffer == nil {
|
||||
return 0
|
||||
}
|
||||
return atomic.LoadUint64(&buffer.dropped)
|
||||
}
|
||||
|
||||
func (buffer *Buffer) Snapshot() []interface{} {
|
||||
if buffer == nil {
|
||||
return nil
|
||||
}
|
||||
buffer.mu.RLock()
|
||||
defer buffer.mu.RUnlock()
|
||||
result := make([]interface{}, len(buffer.items))
|
||||
copy(result, buffer.items)
|
||||
return result
|
||||
}
|
||||
|
||||
func (buffer *Buffer) Last() (interface{}, bool) {
|
||||
if buffer == nil {
|
||||
return nil, false
|
||||
}
|
||||
buffer.mu.RLock()
|
||||
defer buffer.mu.RUnlock()
|
||||
if len(buffer.items) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
return buffer.items[len(buffer.items)-1], true
|
||||
}
|
||||
|
||||
func (buffer *Buffer) TakeAll() []interface{} {
|
||||
if buffer == nil {
|
||||
return nil
|
||||
}
|
||||
buffer.mu.Lock()
|
||||
defer buffer.mu.Unlock()
|
||||
if len(buffer.items) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make([]interface{}, len(buffer.items))
|
||||
copy(result, buffer.items)
|
||||
buffer.items = buffer.items[:0]
|
||||
return result
|
||||
}
|
||||
|
||||
func (buffer *Buffer) Reset() {
|
||||
if buffer == nil {
|
||||
return
|
||||
}
|
||||
buffer.mu.Lock()
|
||||
buffer.items = buffer.items[:0]
|
||||
buffer.mu.Unlock()
|
||||
atomic.StoreUint64(&buffer.dropped, 0)
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package pipelinex
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Entry struct {
|
||||
Time time.Time
|
||||
LevelName string
|
||||
LoggerName string
|
||||
Thread string
|
||||
File string
|
||||
Line int
|
||||
Func string
|
||||
Message string
|
||||
Error string
|
||||
Fields map[string]interface{}
|
||||
}
|
||||
|
||||
type TextOptions struct {
|
||||
IncludeTimestamp bool
|
||||
IncludeLevel bool
|
||||
IncludeSource bool
|
||||
IncludeThread bool
|
||||
IncludeLogger bool
|
||||
}
|
||||
|
||||
func cloneFields(fields map[string]interface{}) map[string]interface{} {
|
||||
if len(fields) == 0 {
|
||||
return nil
|
||||
}
|
||||
cloned := make(map[string]interface{}, len(fields))
|
||||
for key, value := range fields {
|
||||
cloned[key] = value
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
func renderFields(fields map[string]interface{}) string {
|
||||
if len(fields) == 0 {
|
||||
return ""
|
||||
}
|
||||
keys := make([]string, 0, len(fields))
|
||||
for key := range fields {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
pairs := make([]string, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
pairs = append(pairs, fmt.Sprintf("%s=%v", key, fields[key]))
|
||||
}
|
||||
return strings.Join(pairs, " ")
|
||||
}
|
||||
|
||||
func FormatText(entry Entry, options TextOptions) ([]byte, error) {
|
||||
parts := make([]string, 0, 6)
|
||||
if options.IncludeTimestamp {
|
||||
if !entry.Time.IsZero() {
|
||||
parts = append(parts, entry.Time.Format("2006-01-02 15:04:05.000000"))
|
||||
}
|
||||
}
|
||||
if options.IncludeSource {
|
||||
source := ""
|
||||
if entry.File != "" {
|
||||
source = fmt.Sprintf("%s:%d", entry.File, entry.Line)
|
||||
}
|
||||
if entry.Func != "" {
|
||||
if source != "" {
|
||||
source += " "
|
||||
}
|
||||
source += "<" + entry.Func + ">"
|
||||
}
|
||||
if source != "" {
|
||||
parts = append(parts, source)
|
||||
}
|
||||
}
|
||||
if options.IncludeThread && entry.Thread != "" {
|
||||
parts = append(parts, "|"+entry.Thread+"|")
|
||||
}
|
||||
if options.IncludeLevel {
|
||||
if entry.LevelName != "" {
|
||||
parts = append(parts, "["+entry.LevelName+"]")
|
||||
}
|
||||
}
|
||||
if options.IncludeLogger && entry.LoggerName != "" {
|
||||
parts = append(parts, "logger="+entry.LoggerName)
|
||||
}
|
||||
|
||||
messageParts := make([]string, 0, 3)
|
||||
if entry.Message != "" {
|
||||
messageParts = append(messageParts, entry.Message)
|
||||
}
|
||||
if entry.Error != "" {
|
||||
messageParts = append(messageParts, "error="+entry.Error)
|
||||
}
|
||||
fieldText := renderFields(entry.Fields)
|
||||
if fieldText != "" {
|
||||
messageParts = append(messageParts, fieldText)
|
||||
}
|
||||
if len(messageParts) > 0 {
|
||||
parts = append(parts, strings.Join(messageParts, " "))
|
||||
}
|
||||
return []byte(strings.Join(parts, " ")), nil
|
||||
}
|
||||
|
||||
func FormatJSON(entry Entry, pretty bool) ([]byte, error) {
|
||||
payload := map[string]interface{}{
|
||||
"time": entry.Time.Format(time.RFC3339Nano),
|
||||
"level": entry.LevelName,
|
||||
"msg": entry.Message,
|
||||
"logger": entry.LoggerName,
|
||||
"thread": entry.Thread,
|
||||
}
|
||||
if entry.File != "" {
|
||||
payload["file"] = entry.File
|
||||
}
|
||||
if entry.Line > 0 {
|
||||
payload["line"] = entry.Line
|
||||
}
|
||||
if entry.Func != "" {
|
||||
payload["func"] = entry.Func
|
||||
}
|
||||
if entry.Error != "" {
|
||||
payload["error"] = entry.Error
|
||||
}
|
||||
if len(entry.Fields) > 0 {
|
||||
payload["fields"] = cloneFields(entry.Fields)
|
||||
}
|
||||
if pretty {
|
||||
return json.MarshalIndent(payload, "", " ")
|
||||
}
|
||||
return json.Marshal(payload)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package redactutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func NormalizeMask(mask string) string {
|
||||
mask = strings.TrimSpace(mask)
|
||||
if mask == "" {
|
||||
return "[REDACTED]"
|
||||
}
|
||||
return mask
|
||||
}
|
||||
|
||||
func BuildFieldSet(fields ...string) map[string]struct{} {
|
||||
fieldMap := make(map[string]struct{}, len(fields))
|
||||
for _, field := range fields {
|
||||
field = strings.TrimSpace(strings.ToLower(field))
|
||||
if field == "" {
|
||||
continue
|
||||
}
|
||||
fieldMap[field] = struct{}{}
|
||||
}
|
||||
return fieldMap
|
||||
}
|
||||
|
||||
func LookupFieldKey(key string) string {
|
||||
return strings.TrimSpace(strings.ToLower(key))
|
||||
}
|
||||
|
||||
func MaskFields(fields map[string]interface{}, mask string) map[string]interface{} {
|
||||
if len(fields) == 0 {
|
||||
return nil
|
||||
}
|
||||
mask = NormalizeMask(mask)
|
||||
masked := make(map[string]interface{}, len(fields))
|
||||
for key := range fields {
|
||||
masked[key] = mask
|
||||
}
|
||||
return masked
|
||||
}
|
||||
|
||||
func ReplaceRegex(pattern *regexp.Regexp, text string, replacement string) (string, bool) {
|
||||
if pattern == nil || text == "" {
|
||||
return text, false
|
||||
}
|
||||
if replacement == "" {
|
||||
replacement = "[REDACTED]"
|
||||
}
|
||||
changed := pattern.ReplaceAllString(text, replacement)
|
||||
return changed, changed != text
|
||||
}
|
||||
|
||||
func IsMasked(value interface{}, mask string) bool {
|
||||
return fmt.Sprint(value) == mask
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package rotatemanage
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
MaxBackups int
|
||||
MaxAge time.Duration
|
||||
Compress bool
|
||||
Pattern string
|
||||
}
|
||||
|
||||
type backupFileMeta struct {
|
||||
path string
|
||||
modTime time.Time
|
||||
}
|
||||
|
||||
func Apply(archivePath string, currentPath string, options Options) error {
|
||||
if archivePath == "" || currentPath == "" {
|
||||
return nil
|
||||
}
|
||||
if options.Compress {
|
||||
if _, err := gzipBackupFile(archivePath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
backups, err := listManagedBackups(currentPath, options.Pattern)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cleanupManagedBackups(backups, options)
|
||||
}
|
||||
|
||||
func listManagedBackups(currentPath string, pattern string) ([]backupFileMeta, error) {
|
||||
dir := filepath.Dir(currentPath)
|
||||
base := filepath.Base(currentPath)
|
||||
stem := strings.TrimSuffix(base, filepath.Ext(base))
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
backups := make([]backupFileMeta, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := entry.Name()
|
||||
if name == base {
|
||||
continue
|
||||
}
|
||||
matched, err := IsManagedBackupName(name, base, stem, pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !matched {
|
||||
continue
|
||||
}
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
backups = append(backups, backupFileMeta{
|
||||
path: filepath.Join(dir, name),
|
||||
modTime: info.ModTime(),
|
||||
})
|
||||
}
|
||||
return backups, nil
|
||||
}
|
||||
|
||||
func IsManagedBackupName(name string, base string, stem string, pattern string) (bool, error) {
|
||||
if pattern != "" {
|
||||
return filepath.Match(pattern, name)
|
||||
}
|
||||
prefixes := []string{
|
||||
base + ".",
|
||||
base + "_",
|
||||
base + "-",
|
||||
}
|
||||
if stem != "" && stem != base {
|
||||
prefixes = append(prefixes,
|
||||
stem+".",
|
||||
stem+"_",
|
||||
stem+"-",
|
||||
)
|
||||
}
|
||||
for _, prefix := range prefixes {
|
||||
if !strings.HasPrefix(name, prefix) {
|
||||
continue
|
||||
}
|
||||
if isLikelyManagedBackupSuffix(strings.TrimPrefix(name, prefix)) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func isLikelyManagedBackupSuffix(suffix string) bool {
|
||||
suffix = strings.TrimSpace(strings.ToLower(suffix))
|
||||
if suffix == "" {
|
||||
return false
|
||||
}
|
||||
suffix = strings.TrimSuffix(suffix, ".gz")
|
||||
suffix = strings.TrimSuffix(suffix, ".zip")
|
||||
if suffix == "" {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(suffix, "bak") {
|
||||
return true
|
||||
}
|
||||
for _, ch := range suffix {
|
||||
if ch >= '0' && ch <= '9' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func cleanupManagedBackups(backups []backupFileMeta, options Options) error {
|
||||
var firstErr error
|
||||
now := time.Now()
|
||||
kept := make([]backupFileMeta, 0, len(backups))
|
||||
for _, item := range backups {
|
||||
if options.MaxAge > 0 && now.Sub(item.modTime) > options.MaxAge {
|
||||
if err := os.Remove(item.path); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
continue
|
||||
}
|
||||
kept = append(kept, item)
|
||||
}
|
||||
if options.MaxBackups > 0 && len(kept) > options.MaxBackups {
|
||||
sort.Slice(kept, func(i, j int) bool {
|
||||
return kept[i].modTime.After(kept[j].modTime)
|
||||
})
|
||||
for _, item := range kept[options.MaxBackups:] {
|
||||
if err := os.Remove(item.path); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
}
|
||||
return firstErr
|
||||
}
|
||||
|
||||
func gzipBackupFile(path string) (string, error) {
|
||||
if strings.HasSuffix(path, ".gz") {
|
||||
return path, nil
|
||||
}
|
||||
source, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
destination := path + ".gz"
|
||||
temp := destination + ".tmp"
|
||||
target, err := os.Create(temp)
|
||||
if err != nil {
|
||||
_ = source.Close()
|
||||
return "", err
|
||||
}
|
||||
|
||||
gzWriter := gzip.NewWriter(target)
|
||||
if _, err = io.Copy(gzWriter, source); err != nil {
|
||||
_ = source.Close()
|
||||
_ = gzWriter.Close()
|
||||
_ = target.Close()
|
||||
_ = os.Remove(temp)
|
||||
return "", err
|
||||
}
|
||||
if err = gzWriter.Close(); err != nil {
|
||||
_ = source.Close()
|
||||
_ = target.Close()
|
||||
_ = os.Remove(temp)
|
||||
return "", err
|
||||
}
|
||||
if err = target.Close(); err != nil {
|
||||
_ = source.Close()
|
||||
_ = os.Remove(temp)
|
||||
return "", err
|
||||
}
|
||||
if err = source.Close(); err != nil {
|
||||
_ = os.Remove(temp)
|
||||
return "", err
|
||||
}
|
||||
if err = os.Rename(temp, destination); err != nil {
|
||||
_ = os.Remove(temp)
|
||||
return "", err
|
||||
}
|
||||
if err = os.Remove(path); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return destination, nil
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package routerx
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Matcher func(level int) bool
|
||||
|
||||
type Route struct {
|
||||
Index int
|
||||
Name string
|
||||
Match Matcher
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
type Snapshot struct {
|
||||
Index int
|
||||
Name string
|
||||
Match Matcher
|
||||
}
|
||||
|
||||
func Normalize(routes []Route) []Snapshot {
|
||||
if len(routes) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make([]Snapshot, 0, len(routes))
|
||||
for _, route := range routes {
|
||||
if !route.Enabled {
|
||||
continue
|
||||
}
|
||||
name := route.Name
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("route-%d", route.Index)
|
||||
}
|
||||
match := route.Match
|
||||
if match == nil {
|
||||
match = MatchAllLevels()
|
||||
}
|
||||
result = append(result, Snapshot{
|
||||
Index: route.Index,
|
||||
Name: name,
|
||||
Match: match,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func MatchAllLevels() Matcher {
|
||||
return func(level int) bool {
|
||||
_ = level
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func MatchLevels(levels ...int) Matcher {
|
||||
levelSet := make(map[int]struct{}, len(levels))
|
||||
for _, level := range levels {
|
||||
levelSet[level] = struct{}{}
|
||||
}
|
||||
return func(level int) bool {
|
||||
_, ok := levelSet[level]
|
||||
return ok
|
||||
}
|
||||
}
|
||||
|
||||
func MatchAtLeast(minLevel int) Matcher {
|
||||
return func(level int) bool {
|
||||
return level >= minLevel
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package runtimex
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrStackClosed = errors.New("stack closed")
|
||||
ErrStackFull = errors.New("stack full")
|
||||
)
|
||||
|
||||
type ChanStack struct {
|
||||
data chan interface{}
|
||||
cap uint64
|
||||
current uint64
|
||||
isClose atomic.Value
|
||||
}
|
||||
|
||||
func NewChanStack(cap uint64) *ChanStack {
|
||||
rtnBuffer := new(ChanStack)
|
||||
rtnBuffer.cap = cap
|
||||
rtnBuffer.isClose.Store(false)
|
||||
rtnBuffer.data = make(chan interface{}, cap)
|
||||
return rtnBuffer
|
||||
}
|
||||
|
||||
func (s *ChanStack) init() {
|
||||
s.cap = 1024
|
||||
s.data = make(chan interface{}, s.cap)
|
||||
s.isClose.Store(false)
|
||||
}
|
||||
|
||||
func (s *ChanStack) Free() uint64 {
|
||||
return s.cap - atomic.LoadUint64(&s.current)
|
||||
}
|
||||
|
||||
func (s *ChanStack) Cap() uint64 {
|
||||
return s.cap
|
||||
}
|
||||
|
||||
func (s *ChanStack) Len() uint64 {
|
||||
return atomic.LoadUint64(&s.current)
|
||||
}
|
||||
|
||||
func (s *ChanStack) Pop() (interface{}, error) {
|
||||
if s.isClose.Load() == nil {
|
||||
s.init()
|
||||
}
|
||||
if s.isClose.Load().(bool) {
|
||||
return nil, io.EOF
|
||||
}
|
||||
data, ok := <-s.data
|
||||
if !ok {
|
||||
s.isClose.Store(true)
|
||||
return nil, io.EOF
|
||||
}
|
||||
for {
|
||||
current := atomic.LoadUint64(&s.current)
|
||||
if current == 0 {
|
||||
break
|
||||
}
|
||||
if atomic.CompareAndSwapUint64(&s.current, current, current-1) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (s *ChanStack) Push(data interface{}) error {
|
||||
if s.isClose.Load() == nil {
|
||||
s.init()
|
||||
}
|
||||
if s.isClose.Load().(bool) {
|
||||
return io.EOF
|
||||
}
|
||||
if err := func() (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = io.EOF
|
||||
}
|
||||
}()
|
||||
s.data <- data
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
for {
|
||||
current := atomic.LoadUint64(&s.current)
|
||||
if atomic.CompareAndSwapUint64(&s.current, current, current+1) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ChanStack) TryPush(data interface{}) error {
|
||||
if s.isClose.Load() == nil {
|
||||
s.init()
|
||||
}
|
||||
if s.isClose.Load().(bool) {
|
||||
return io.EOF
|
||||
}
|
||||
if err := func() (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = io.EOF
|
||||
}
|
||||
}()
|
||||
select {
|
||||
case s.data <- data:
|
||||
return nil
|
||||
default:
|
||||
return ErrStackFull
|
||||
}
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
for {
|
||||
current := atomic.LoadUint64(&s.current)
|
||||
if atomic.CompareAndSwapUint64(&s.current, current, current+1) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ChanStack) Close() error {
|
||||
if s.isClose.Load() == nil {
|
||||
s.init()
|
||||
}
|
||||
if s.isClose.Load().(bool) {
|
||||
return ErrStackClosed
|
||||
}
|
||||
s.isClose.Store(true)
|
||||
defer func() {
|
||||
recover()
|
||||
}()
|
||||
close(s.data)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package runtimex
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestChanStackPushPop(t *testing.T) {
|
||||
stack := NewChanStack(2)
|
||||
if err := stack.Push("a"); err != nil {
|
||||
t.Fatalf("Push failed: %v", err)
|
||||
}
|
||||
if err := stack.Push("b"); err != nil {
|
||||
t.Fatalf("Push failed: %v", err)
|
||||
}
|
||||
if stack.Len() != 2 {
|
||||
t.Fatalf("expected len=2, got %d", stack.Len())
|
||||
}
|
||||
if stack.Free() != 0 {
|
||||
t.Fatalf("expected free=0, got %d", stack.Free())
|
||||
}
|
||||
|
||||
first, err := stack.Pop()
|
||||
if err != nil {
|
||||
t.Fatalf("Pop failed: %v", err)
|
||||
}
|
||||
if first.(string) != "a" {
|
||||
t.Fatalf("unexpected first value: %v", first)
|
||||
}
|
||||
second, err := stack.Pop()
|
||||
if err != nil {
|
||||
t.Fatalf("Pop failed: %v", err)
|
||||
}
|
||||
if second.(string) != "b" {
|
||||
t.Fatalf("unexpected second value: %v", second)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChanStackTryPushFull(t *testing.T) {
|
||||
stack := NewChanStack(1)
|
||||
if err := stack.TryPush("a"); err != nil {
|
||||
t.Fatalf("TryPush should succeed on empty stack: %v", err)
|
||||
}
|
||||
if err := stack.TryPush("b"); !errors.Is(err, ErrStackFull) {
|
||||
t.Fatalf("TryPush should return ErrStackFull, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChanStackCloseBehavior(t *testing.T) {
|
||||
stack := NewChanStack(1)
|
||||
if err := stack.Close(); err != nil {
|
||||
t.Fatalf("Close should succeed first time: %v", err)
|
||||
}
|
||||
if err := stack.Close(); !errors.Is(err, ErrStackClosed) {
|
||||
t.Fatalf("Close should return ErrStackClosed on second call, got %v", err)
|
||||
}
|
||||
if err := stack.Push("x"); !errors.Is(err, io.EOF) {
|
||||
t.Fatalf("Push after close should return io.EOF, got %v", err)
|
||||
}
|
||||
if _, err := stack.Pop(); !errors.Is(err, io.EOF) {
|
||||
t.Fatalf("Pop after close should return io.EOF, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package runtimex
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type MapKV struct {
|
||||
kvMap map[interface{}]interface{}
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewMapKV() MapKV {
|
||||
var mp MapKV
|
||||
mp.kvMap = make(map[interface{}]interface{})
|
||||
return mp
|
||||
}
|
||||
|
||||
func (m *MapKV) Get(key interface{}) (interface{}, error) {
|
||||
var err error
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
data, ok := m.kvMap[key]
|
||||
if !ok {
|
||||
err = os.ErrNotExist
|
||||
}
|
||||
return data, err
|
||||
}
|
||||
|
||||
func (m *MapKV) MustGet(key interface{}) interface{} {
|
||||
result, _ := m.Get(key)
|
||||
return result
|
||||
}
|
||||
|
||||
func (m *MapKV) Store(key interface{}, value interface{}) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.kvMap[key] = value
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MapKV) Exists(key interface{}) bool {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
_, ok := m.kvMap[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (m *MapKV) Delete(key interface{}) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delete(m.kvMap, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MapKV) Range(run func(k interface{}, v interface{}) bool) error {
|
||||
for k, v := range m.kvMap {
|
||||
if !run(k, v) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package stdlibx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type LevelMapper func(text string, fallbackLevel int) int
|
||||
|
||||
type Options struct {
|
||||
Prefix string
|
||||
Flags int
|
||||
ShowStd bool
|
||||
TrimNewline bool
|
||||
LevelMapper LevelMapper
|
||||
}
|
||||
|
||||
type Option func(*Options)
|
||||
|
||||
func DefaultOptions() Options {
|
||||
return Options{
|
||||
Prefix: "",
|
||||
Flags: 0,
|
||||
ShowStd: false,
|
||||
TrimNewline: true,
|
||||
LevelMapper: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func WithPrefix(prefix string) Option {
|
||||
return func(options *Options) {
|
||||
if options == nil {
|
||||
return
|
||||
}
|
||||
options.Prefix = prefix
|
||||
}
|
||||
}
|
||||
|
||||
func WithFlags(flags int) Option {
|
||||
return func(options *Options) {
|
||||
if options == nil {
|
||||
return
|
||||
}
|
||||
options.Flags = flags
|
||||
}
|
||||
}
|
||||
|
||||
func WithShowStd(show bool) Option {
|
||||
return func(options *Options) {
|
||||
if options == nil {
|
||||
return
|
||||
}
|
||||
options.ShowStd = show
|
||||
}
|
||||
}
|
||||
|
||||
func WithTrimNewline(trim bool) Option {
|
||||
return func(options *Options) {
|
||||
if options == nil {
|
||||
return
|
||||
}
|
||||
options.TrimNewline = trim
|
||||
}
|
||||
}
|
||||
|
||||
func WithLevelMapper(mapper LevelMapper) Option {
|
||||
return func(options *Options) {
|
||||
if options == nil {
|
||||
return
|
||||
}
|
||||
options.LevelMapper = mapper
|
||||
}
|
||||
}
|
||||
|
||||
func NormalizeOptions(opts []Option) Options {
|
||||
options := DefaultOptions()
|
||||
for _, option := range opts {
|
||||
if option == nil {
|
||||
continue
|
||||
}
|
||||
option(&options)
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
type EmitFunc func(level int, showStd bool, text string)
|
||||
|
||||
type Writer struct {
|
||||
level int
|
||||
showStd bool
|
||||
trimNewline bool
|
||||
levelMapper LevelMapper
|
||||
emit EmitFunc
|
||||
}
|
||||
|
||||
func NewWriter(level int, options Options, emit EmitFunc) *Writer {
|
||||
return &Writer{
|
||||
level: level,
|
||||
showStd: options.ShowStd,
|
||||
trimNewline: options.TrimNewline,
|
||||
levelMapper: options.LevelMapper,
|
||||
emit: emit,
|
||||
}
|
||||
}
|
||||
|
||||
func (writer *Writer) SetShowStd(show bool) {
|
||||
if writer == nil {
|
||||
return
|
||||
}
|
||||
writer.showStd = show
|
||||
}
|
||||
|
||||
func (writer *Writer) SetTrimNewline(trim bool) {
|
||||
if writer == nil {
|
||||
return
|
||||
}
|
||||
writer.trimNewline = trim
|
||||
}
|
||||
|
||||
func (writer *Writer) Write(data []byte) (int, error) {
|
||||
if writer == nil || writer.emit == nil {
|
||||
return 0, errors.New("level writer logger is nil")
|
||||
}
|
||||
text := string(data)
|
||||
if writer.trimNewline {
|
||||
text = strings.TrimRight(text, "\r\n")
|
||||
}
|
||||
if text != "" {
|
||||
level := writer.level
|
||||
if writer.levelMapper != nil {
|
||||
level = writer.levelMapper(text, level)
|
||||
}
|
||||
writer.emit(level, writer.showStd, text)
|
||||
}
|
||||
return len(data), nil
|
||||
}
|
||||
Reference in New Issue
Block a user