重构代码

This commit is contained in:
2026-03-19 16:37:57 +08:00
parent 51608601cf
commit 8023bfe328
75 changed files with 13325 additions and 394 deletions
+88
View File
@@ -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)
}
+58
View File
@@ -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
}
+27
View File
@@ -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()
}
+22
View File
@@ -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)
}
+22
View File
@@ -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)
}
+20
View File
@@ -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())
}
+338
View File
@@ -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])
}
+123
View File
@@ -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)
}
+137
View File
@@ -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)
}
+58
View File
@@ -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
}
+199
View File
@@ -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
}
+68
View File
@@ -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
}
}
+142
View File
@@ -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
}
+64
View File
@@ -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)
}
}
+63
View File
@@ -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
}
+136
View File
@@ -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
}