v0.1.4 formatter race fix, fiber adapter added, default config changed, docs updated
This commit is contained in:
@ -12,7 +12,7 @@ A high-performance, buffered, rotating file logger for Go applications with buil
|
||||
- **Automatic file rotation** and disk space management
|
||||
- **Operational heartbeats** for production monitoring
|
||||
- **Hot reconfiguration** without data loss
|
||||
- **Framework adapters** for gnet v2 and fasthttp
|
||||
- **Framework adapters** for gnet v2, fasthttp, Fiber v2
|
||||
- **Production-grade reliability** with graceful shutdown
|
||||
|
||||
## Quick Start
|
||||
|
||||
@ -72,6 +72,7 @@ func TestBuilder_Build(t *testing.T) {
|
||||
invalidDir := filepath.Join("/root", "unwritable-log-test-dir")
|
||||
logger, err := NewBuilder().
|
||||
Directory(invalidDir).
|
||||
EnableFile(true).
|
||||
Build()
|
||||
|
||||
// Assert that ApplyConfig (called by Build) failed
|
||||
|
||||
@ -98,6 +98,15 @@ func (b *Builder) BuildFastHTTP(opts ...FastHTTPOption) (*FastHTTPAdapter, error
|
||||
return NewFastHTTPAdapter(l, opts...), nil
|
||||
}
|
||||
|
||||
// BuildFiber creates a Fiber v2.54.x adapter
|
||||
func (b *Builder) BuildFiber(opts ...FiberOption) (*FiberAdapter, error) {
|
||||
l, err := b.getLogger()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewFiberAdapter(l, opts...), nil
|
||||
}
|
||||
|
||||
// GetLogger returns the underlying *log.Logger instance
|
||||
// If a logger has not been provided or created yet, it will be initialized
|
||||
func (b *Builder) GetLogger() (*log.Logger, error) {
|
||||
@ -106,7 +115,7 @@ func (b *Builder) GetLogger() (*log.Logger, error) {
|
||||
|
||||
// --- Example Usage ---
|
||||
//
|
||||
// The following demonstrates how to integrate lixenwraith/log with gnet and fasthttp
|
||||
// The following demonstrates how to integrate lixenwraith/log with gnet, fasthttp, and Fiber
|
||||
// using a single, shared logger instance
|
||||
//
|
||||
// // 1. Create and configure application's main logger
|
||||
@ -127,6 +136,9 @@ func (b *Builder) GetLogger() (*log.Logger, error) {
|
||||
// fasthttpLogger, err := builder.BuildFastHTTP()
|
||||
// if err != nil { /* handle error */ }
|
||||
//
|
||||
// fiberLogger, err := builder.BuildFiber()
|
||||
// if err != nil { /* handle error */ }
|
||||
//
|
||||
// // 4. Configure your servers with the adapters
|
||||
//
|
||||
// // For gnet:
|
||||
@ -143,3 +155,15 @@ func (b *Builder) GetLogger() (*log.Logger, error) {
|
||||
// Logger: fasthttpLogger,
|
||||
// }
|
||||
// go server.ListenAndServe(":8080")
|
||||
//
|
||||
// // For Fiber v2.54.x:
|
||||
// // The adapter is passed to fiber.New() via the config
|
||||
// app := fiber.New(fiber.Config{
|
||||
// AppName: "My Application",
|
||||
// })
|
||||
// app.UpdateConfig(fiber.Config{
|
||||
// AppName: "My Application",
|
||||
// })
|
||||
// // Note: Set the logger after app creation if needed
|
||||
// // fiber uses internal logging, adapter can be used in custom middleware
|
||||
// go app.Listen(":3000")
|
||||
@ -22,6 +22,7 @@ func createTestCompatBuilder(t *testing.T) (*Builder, *log.Logger, string) {
|
||||
Directory(tmpDir).
|
||||
Format("json").
|
||||
LevelString("debug").
|
||||
EnableFile(true).
|
||||
Build()
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -225,3 +226,125 @@ func TestFastHTTPAdapter(t *testing.T) {
|
||||
assert.Equal(t, "fasthttp", fields[3])
|
||||
}
|
||||
}
|
||||
|
||||
// TestFiberAdapter tests the Fiber adapter's logging output across all log levels
|
||||
func TestFiberAdapter(t *testing.T) {
|
||||
builder, logger, tmpDir := createTestCompatBuilder(t)
|
||||
defer logger.Shutdown()
|
||||
|
||||
var fatalCalled bool
|
||||
var panicCalled bool
|
||||
adapter, err := builder.BuildFiber(
|
||||
WithFiberFatalHandler(func(msg string) {
|
||||
fatalCalled = true
|
||||
}),
|
||||
WithFiberPanicHandler(func(msg string) {
|
||||
panicCalled = true
|
||||
}),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test formatted logging (Tracef, Debugf, Infof, Warnf, Errorf, Fatalf, Panicf)
|
||||
adapter.Tracef("fiber trace id=%d", 1)
|
||||
adapter.Debugf("fiber debug id=%d", 2)
|
||||
adapter.Infof("fiber info id=%d", 3)
|
||||
adapter.Warnf("fiber warn id=%d", 4)
|
||||
adapter.Errorf("fiber error id=%d", 5)
|
||||
adapter.Fatalf("fiber fatal id=%d", 6)
|
||||
adapter.Panicf("fiber panic id=%d", 7)
|
||||
|
||||
err = logger.Flush(time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
lines := readLogFile(t, tmpDir, 7)
|
||||
|
||||
expected := []struct {
|
||||
level string
|
||||
msg string
|
||||
}{
|
||||
{"DEBUG", "fiber trace id=1"},
|
||||
{"DEBUG", "fiber debug id=2"},
|
||||
{"INFO", "fiber info id=3"},
|
||||
{"WARN", "fiber warn id=4"},
|
||||
{"ERROR", "fiber error id=5"},
|
||||
{"ERROR", "fiber fatal id=6"},
|
||||
{"ERROR", "fiber panic id=7"},
|
||||
}
|
||||
|
||||
require.Len(t, lines, 7, "Should have 7 fiber log lines")
|
||||
|
||||
for i, line := range lines {
|
||||
var entry map[string]any
|
||||
err := json.Unmarshal([]byte(line), &entry)
|
||||
require.NoError(t, err, "Failed to parse log line: %s", line)
|
||||
|
||||
assert.Equal(t, expected[i].level, entry["level"])
|
||||
fields := entry["fields"].([]any)
|
||||
assert.Equal(t, "msg", fields[0])
|
||||
assert.Equal(t, expected[i].msg, fields[1])
|
||||
assert.Equal(t, "source", fields[2])
|
||||
assert.Equal(t, "fiber", fields[3])
|
||||
}
|
||||
assert.True(t, fatalCalled, "Custom fatal handler should have been called")
|
||||
assert.True(t, panicCalled, "Custom panic handler should have been called")
|
||||
}
|
||||
|
||||
// TestFiberAdapterStructuredLogging tests Fiber's structured logging (WithLogger methods)
|
||||
func TestFiberAdapterStructuredLogging(t *testing.T) {
|
||||
builder, logger, tmpDir := createTestCompatBuilder(t)
|
||||
defer logger.Shutdown()
|
||||
|
||||
adapter, err := builder.BuildFiber()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test structured logging with key-value pairs
|
||||
adapter.Infow("request served", "status", 200, "client_ip", "127.0.0.1", "method", "GET")
|
||||
adapter.Debugw("query executed", "duration_ms", 42, "query", "SELECT * FROM users")
|
||||
|
||||
err = logger.Flush(time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
lines := readLogFile(t, tmpDir, 2)
|
||||
require.Len(t, lines, 2, "Should have 2 fiber structured log lines")
|
||||
|
||||
// Check first structured log (Infow)
|
||||
var entry1 map[string]any
|
||||
err = json.Unmarshal([]byte(lines[0]), &entry1)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "INFO", entry1["level"])
|
||||
fields1 := entry1["fields"].([]any)
|
||||
assert.Equal(t, "msg", fields1[0])
|
||||
assert.Equal(t, "request served", fields1[1])
|
||||
assert.Equal(t, "source", fields1[2])
|
||||
assert.Equal(t, "fiber", fields1[3])
|
||||
assert.Equal(t, "status", fields1[4])
|
||||
assert.Equal(t, 200.0, fields1[5]) // JSON numbers are float64
|
||||
assert.Equal(t, "client_ip", fields1[6])
|
||||
assert.Equal(t, "127.0.0.1", fields1[7])
|
||||
|
||||
// Check second structured log (Debugw)
|
||||
var entry2 map[string]any
|
||||
err = json.Unmarshal([]byte(lines[1]), &entry2)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "DEBUG", entry2["level"])
|
||||
fields2 := entry2["fields"].([]any)
|
||||
assert.Equal(t, "msg", fields2[0])
|
||||
assert.Equal(t, "query executed", fields2[1])
|
||||
assert.Equal(t, "source", fields2[2])
|
||||
assert.Equal(t, "fiber", fields2[3])
|
||||
assert.Equal(t, "duration_ms", fields2[4])
|
||||
assert.Equal(t, 42.0, fields2[5]) // JSON numbers are float64
|
||||
}
|
||||
|
||||
// TestFiberBuilderIntegration ensures Fiber adapter can be built from builder
|
||||
func TestFiberBuilderIntegration(t *testing.T) {
|
||||
builder, logger, _ := createTestCompatBuilder(t)
|
||||
defer logger.Shutdown()
|
||||
|
||||
fiberAdapter, err := builder.BuildFiber()
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, fiberAdapter)
|
||||
assert.Equal(t, logger, fiberAdapter.logger)
|
||||
}
|
||||
254
compat/fiber.go
Normal file
254
compat/fiber.go
Normal file
@ -0,0 +1,254 @@
|
||||
// FILE: lixenwraith/log/compat/fiber.go
|
||||
package compat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/lixenwraith/log"
|
||||
)
|
||||
|
||||
// FiberAdapter wraps lixenwraith/log.Logger to implement Fiber's CommonLogger interface
|
||||
// This provides compatibility with Fiber v2.54.x logging requirements
|
||||
type FiberAdapter struct {
|
||||
logger *log.Logger
|
||||
fatalHandler func(msg string) // Customizable fatal behavior
|
||||
panicHandler func(msg string) // Customizable panic behavior
|
||||
}
|
||||
|
||||
// NewFiberAdapter creates a new Fiber-compatible logger adapter
|
||||
func NewFiberAdapter(logger *log.Logger, opts ...FiberOption) *FiberAdapter {
|
||||
adapter := &FiberAdapter{
|
||||
logger: logger,
|
||||
fatalHandler: func(msg string) {
|
||||
os.Exit(1) // Default behavior
|
||||
},
|
||||
panicHandler: func(msg string) {
|
||||
panic(msg) // Default behavior
|
||||
},
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(adapter)
|
||||
}
|
||||
|
||||
return adapter
|
||||
}
|
||||
|
||||
// FiberOption allows customizing adapter behavior
|
||||
type FiberOption func(*FiberAdapter)
|
||||
|
||||
// WithFiberFatalHandler sets a custom fatal handler
|
||||
func WithFiberFatalHandler(handler func(string)) FiberOption {
|
||||
return func(a *FiberAdapter) {
|
||||
a.fatalHandler = handler
|
||||
}
|
||||
}
|
||||
|
||||
// WithFiberPanicHandler sets a custom panic handler
|
||||
func WithFiberPanicHandler(handler func(string)) FiberOption {
|
||||
return func(a *FiberAdapter) {
|
||||
a.panicHandler = handler
|
||||
}
|
||||
}
|
||||
|
||||
// --- Logger interface implementation (7 methods) ---
|
||||
|
||||
// Trace logs at trace/debug level
|
||||
func (a *FiberAdapter) Trace(v ...any) {
|
||||
msg := fmt.Sprint(v...)
|
||||
a.logger.Debug("msg", msg, "source", "fiber", "level", "trace")
|
||||
}
|
||||
|
||||
// Debug logs at debug level
|
||||
func (a *FiberAdapter) Debug(v ...any) {
|
||||
msg := fmt.Sprint(v...)
|
||||
a.logger.Debug("msg", msg, "source", "fiber")
|
||||
}
|
||||
|
||||
// Info logs at info level
|
||||
func (a *FiberAdapter) Info(v ...any) {
|
||||
msg := fmt.Sprint(v...)
|
||||
a.logger.Info("msg", msg, "source", "fiber")
|
||||
}
|
||||
|
||||
// Warn logs at warn level
|
||||
func (a *FiberAdapter) Warn(v ...any) {
|
||||
msg := fmt.Sprint(v...)
|
||||
a.logger.Warn("msg", msg, "source", "fiber")
|
||||
}
|
||||
|
||||
// Error logs at error level
|
||||
func (a *FiberAdapter) Error(v ...any) {
|
||||
msg := fmt.Sprint(v...)
|
||||
a.logger.Error("msg", msg, "source", "fiber")
|
||||
}
|
||||
|
||||
// Fatal logs at error level and triggers fatal handler
|
||||
func (a *FiberAdapter) Fatal(v ...any) {
|
||||
msg := fmt.Sprint(v...)
|
||||
a.logger.Error("msg", msg, "source", "fiber", "fatal", true)
|
||||
|
||||
// Ensure log is flushed before exit
|
||||
_ = a.logger.Flush(100 * time.Millisecond)
|
||||
|
||||
if a.fatalHandler != nil {
|
||||
a.fatalHandler(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Panic logs at error level and triggers panic handler
|
||||
func (a *FiberAdapter) Panic(v ...any) {
|
||||
msg := fmt.Sprint(v...)
|
||||
a.logger.Error("msg", msg, "source", "fiber", "panic", true)
|
||||
|
||||
// Ensure log is flushed before panic
|
||||
_ = a.logger.Flush(100 * time.Millisecond)
|
||||
|
||||
if a.panicHandler != nil {
|
||||
a.panicHandler(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Write makes FiberAdapter implement io.Writer interface
|
||||
// This allows it to be used with fiber.Config.ErrorHandler output redirection
|
||||
func (a *FiberAdapter) Write(p []byte) (n int, err error) {
|
||||
msg := string(p)
|
||||
// Trim trailing newline if present
|
||||
if len(msg) > 0 && msg[len(msg)-1] == '\n' {
|
||||
msg = msg[:len(msg)-1]
|
||||
}
|
||||
a.logger.Info("msg", msg, "source", "fiber")
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// --- FormatLogger interface implementation (7 methods) ---
|
||||
|
||||
// Tracef logs at trace/debug level with printf-style formatting
|
||||
func (a *FiberAdapter) Tracef(format string, v ...any) {
|
||||
msg := fmt.Sprintf(format, v...)
|
||||
a.logger.Debug("msg", msg, "source", "fiber", "level", "trace")
|
||||
}
|
||||
|
||||
// Debugf logs at debug level with printf-style formatting
|
||||
func (a *FiberAdapter) Debugf(format string, v ...any) {
|
||||
msg := fmt.Sprintf(format, v...)
|
||||
a.logger.Debug("msg", msg, "source", "fiber")
|
||||
}
|
||||
|
||||
// Infof logs at info level with printf-style formatting
|
||||
func (a *FiberAdapter) Infof(format string, v ...any) {
|
||||
msg := fmt.Sprintf(format, v...)
|
||||
a.logger.Info("msg", msg, "source", "fiber")
|
||||
}
|
||||
|
||||
// Warnf logs at warn level with printf-style formatting
|
||||
func (a *FiberAdapter) Warnf(format string, v ...any) {
|
||||
msg := fmt.Sprintf(format, v...)
|
||||
a.logger.Warn("msg", msg, "source", "fiber")
|
||||
}
|
||||
|
||||
// Errorf logs at error level with printf-style formatting
|
||||
func (a *FiberAdapter) Errorf(format string, v ...any) {
|
||||
msg := fmt.Sprintf(format, v...)
|
||||
a.logger.Error("msg", msg, "source", "fiber")
|
||||
}
|
||||
|
||||
// Fatalf logs at error level and triggers fatal handler
|
||||
func (a *FiberAdapter) Fatalf(format string, v ...any) {
|
||||
msg := fmt.Sprintf(format, v...)
|
||||
a.logger.Error("msg", msg, "source", "fiber", "fatal", true)
|
||||
|
||||
// Ensure log is flushed before exit
|
||||
_ = a.logger.Flush(100 * time.Millisecond)
|
||||
|
||||
if a.fatalHandler != nil {
|
||||
a.fatalHandler(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Panicf logs at error level and triggers panic handler
|
||||
func (a *FiberAdapter) Panicf(format string, v ...any) {
|
||||
msg := fmt.Sprintf(format, v...)
|
||||
a.logger.Error("msg", msg, "source", "fiber", "panic", true)
|
||||
|
||||
// Ensure log is flushed before panic
|
||||
_ = a.logger.Flush(100 * time.Millisecond)
|
||||
|
||||
if a.panicHandler != nil {
|
||||
a.panicHandler(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// --- WithLogger interface implementation (7 methods) ---
|
||||
|
||||
// Tracew logs at trace/debug level with structured key-value pairs
|
||||
func (a *FiberAdapter) Tracew(msg string, keysAndValues ...any) {
|
||||
fields := make([]any, 0, len(keysAndValues)+6)
|
||||
fields = append(fields, "msg", msg, "source", "fiber", "level", "trace")
|
||||
fields = append(fields, keysAndValues...)
|
||||
a.logger.Debug(fields...)
|
||||
}
|
||||
|
||||
// Debugw logs at debug level with structured key-value pairs
|
||||
func (a *FiberAdapter) Debugw(msg string, keysAndValues ...any) {
|
||||
fields := make([]any, 0, len(keysAndValues)+4)
|
||||
fields = append(fields, "msg", msg, "source", "fiber")
|
||||
fields = append(fields, keysAndValues...)
|
||||
a.logger.Debug(fields...)
|
||||
}
|
||||
|
||||
// Infow logs at info level with structured key-value pairs
|
||||
func (a *FiberAdapter) Infow(msg string, keysAndValues ...any) {
|
||||
fields := make([]any, 0, len(keysAndValues)+4)
|
||||
fields = append(fields, "msg", msg, "source", "fiber")
|
||||
fields = append(fields, keysAndValues...)
|
||||
a.logger.Info(fields...)
|
||||
}
|
||||
|
||||
// Warnw logs at warn level with structured key-value pairs
|
||||
func (a *FiberAdapter) Warnw(msg string, keysAndValues ...any) {
|
||||
fields := make([]any, 0, len(keysAndValues)+4)
|
||||
fields = append(fields, "msg", msg, "source", "fiber")
|
||||
fields = append(fields, keysAndValues...)
|
||||
a.logger.Warn(fields...)
|
||||
}
|
||||
|
||||
// Errorw logs at error level with structured key-value pairs
|
||||
func (a *FiberAdapter) Errorw(msg string, keysAndValues ...any) {
|
||||
fields := make([]any, 0, len(keysAndValues)+4)
|
||||
fields = append(fields, "msg", msg, "source", "fiber")
|
||||
fields = append(fields, keysAndValues...)
|
||||
a.logger.Error(fields...)
|
||||
}
|
||||
|
||||
// Fatalw logs at error level with structured key-value pairs and triggers fatal handler
|
||||
func (a *FiberAdapter) Fatalw(msg string, keysAndValues ...any) {
|
||||
fields := make([]any, 0, len(keysAndValues)+6)
|
||||
fields = append(fields, "msg", msg, "source", "fiber", "fatal", true)
|
||||
fields = append(fields, keysAndValues...)
|
||||
a.logger.Error(fields...)
|
||||
|
||||
// Ensure log is flushed before exit
|
||||
_ = a.logger.Flush(100 * time.Millisecond)
|
||||
|
||||
if a.fatalHandler != nil {
|
||||
a.fatalHandler(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Panicw logs at error level with structured key-value pairs and triggers panic handler
|
||||
func (a *FiberAdapter) Panicw(msg string, keysAndValues ...any) {
|
||||
fields := make([]any, 0, len(keysAndValues)+6)
|
||||
fields = append(fields, "msg", msg, "source", "fiber", "panic", true)
|
||||
fields = append(fields, keysAndValues...)
|
||||
a.logger.Error(fields...)
|
||||
|
||||
// Ensure log is flushed before panic
|
||||
_ = a.logger.Flush(100 * time.Millisecond)
|
||||
|
||||
if a.panicHandler != nil {
|
||||
a.panicHandler(msg)
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
// FILE: lixenwraith/log/compat/structured.go
|
||||
// FILE: lixenwraith/log/compat/structured_gnet.go
|
||||
package compat
|
||||
|
||||
import (
|
||||
@ -61,8 +61,8 @@ type Config struct {
|
||||
var defaultConfig = Config{
|
||||
// Output settings
|
||||
EnableConsole: true,
|
||||
ConsoleTarget: "stdout",
|
||||
EnableFile: true,
|
||||
ConsoleTarget: "stderr",
|
||||
EnableFile: false,
|
||||
|
||||
// File settings
|
||||
Level: LevelInfo,
|
||||
@ -71,11 +71,11 @@ var defaultConfig = Config{
|
||||
Extension: "log",
|
||||
|
||||
// Formatting
|
||||
Format: "txt",
|
||||
Format: "raw",
|
||||
ShowTimestamp: true,
|
||||
ShowLevel: true,
|
||||
TimestampFormat: time.RFC3339Nano,
|
||||
Sanitization: PolicyTxt,
|
||||
Sanitization: PolicyRaw,
|
||||
|
||||
// Buffer and size limits
|
||||
BufferSize: 1024,
|
||||
|
||||
@ -18,9 +18,10 @@ func TestDefaultConfig(t *testing.T) {
|
||||
assert.NotNil(t, cfg)
|
||||
assert.Equal(t, LevelInfo, cfg.Level)
|
||||
assert.Equal(t, "log", cfg.Name)
|
||||
assert.Equal(t, "./log", cfg.Directory)
|
||||
assert.Equal(t, "txt", cfg.Format)
|
||||
assert.Equal(t, "log", cfg.Extension)
|
||||
assert.Equal(t, "./log", cfg.Directory)
|
||||
assert.Equal(t, "raw", cfg.Format)
|
||||
assert.Equal(t, PolicyRaw, cfg.Sanitization)
|
||||
assert.True(t, cfg.ShowTimestamp)
|
||||
assert.True(t, cfg.ShowLevel)
|
||||
assert.Equal(t, time.RFC3339Nano, cfg.TimestampFormat)
|
||||
|
||||
282
doc/adapters.md
282
doc/adapters.md
@ -350,3 +350,285 @@ func requestLogger(adapter *compat.FastHTTPAdapter) fasthttp.RequestHandler {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Simple integration example suite
|
||||
|
||||
Below simple client and server examples can be used to test the basic functionality of the adapters. They are not included in the package to avoid dependency creep.
|
||||
|
||||
|
||||
#### gnet server
|
||||
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/lixenwraith/log"
|
||||
"github.com/lixenwraith/log/compat"
|
||||
"github.com/panjf2000/gnet/v2"
|
||||
)
|
||||
|
||||
type echoServer struct {
|
||||
gnet.BuiltinEventEngine
|
||||
adapter *compat.GnetAdapter
|
||||
}
|
||||
|
||||
func (es *echoServer) OnTraffic(c gnet.Conn) gnet.Action {
|
||||
buf, _ := c.Next(-1)
|
||||
if len(buf) > 0 {
|
||||
es.adapter.Infof("Echo %d bytes", len(buf))
|
||||
c.Write(buf)
|
||||
}
|
||||
return gnet.None
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Minimal logger config
|
||||
logger, err := log.NewBuilder().
|
||||
Directory("./logs_gnet").
|
||||
Format("json").
|
||||
LevelString("info").
|
||||
HeartbeatLevel(0).
|
||||
Build()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := logger.Start(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
adapter, err := compat.NewBuilder().WithLogger(logger).BuildGnet()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
handler := &echoServer{adapter: adapter}
|
||||
|
||||
fmt.Println("Starting gnet server on :9000")
|
||||
fmt.Println("Press Ctrl+C to stop")
|
||||
|
||||
// Signal handling
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
if err := gnet.Run(handler, "tcp://:9000",
|
||||
gnet.WithLogger(adapter),
|
||||
); err != nil {
|
||||
fmt.Printf("gnet error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
<-sigChan
|
||||
fmt.Println("\nShutting down...")
|
||||
logger.Shutdown()
|
||||
}
|
||||
```
|
||||
|
||||
#### fasthttp server
|
||||
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/lixenwraith/log"
|
||||
"github.com/lixenwraith/log/compat"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Minimal logger config
|
||||
logger, err := log.NewBuilder().
|
||||
Directory("./logs_fasthttp").
|
||||
Format("json").
|
||||
LevelString("info").
|
||||
HeartbeatLevel(0).
|
||||
Build()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := logger.Start(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
adapter, err := compat.NewBuilder().WithLogger(logger).BuildFastHTTP()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
server := &fasthttp.Server{
|
||||
Handler: func(ctx *fasthttp.RequestCtx) {
|
||||
adapter.Printf("Request: %s %s", ctx.Method(), ctx.Path())
|
||||
ctx.WriteString("OK")
|
||||
},
|
||||
Logger: adapter,
|
||||
Name: "TestServer",
|
||||
}
|
||||
|
||||
fmt.Println("Starting FastHTTP server on :8080")
|
||||
fmt.Println("Press Ctrl+C to stop")
|
||||
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
if err := server.ListenAndServe(":8080"); err != nil {
|
||||
fmt.Printf("FastHTTP error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
<-sigChan
|
||||
fmt.Println("\nShutting down...")
|
||||
server.Shutdown()
|
||||
logger.Shutdown()
|
||||
}
|
||||
```
|
||||
|
||||
#### Fiber server
|
||||
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/lixenwraith/log"
|
||||
"github.com/lixenwraith/log/compat"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Minimal logger config
|
||||
logger, err := log.NewBuilder().
|
||||
Directory("./logs_fiber").
|
||||
Format("json").
|
||||
LevelString("info").
|
||||
HeartbeatLevel(0).
|
||||
Build()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := logger.Start(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
adapter, err := compat.NewBuilder().WithLogger(logger).BuildFiber()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
app := fiber.New(fiber.Config{
|
||||
DisableStartupMessage: true,
|
||||
})
|
||||
|
||||
app.Use(func(c *fiber.Ctx) error {
|
||||
adapter.Infow("Request", "method", c.Method(), "path", c.Path())
|
||||
return c.Next()
|
||||
})
|
||||
|
||||
app.Get("/", func(c *fiber.Ctx) error {
|
||||
return c.SendString("OK")
|
||||
})
|
||||
|
||||
fmt.Println("Starting Fiber server on :3000")
|
||||
fmt.Println("Press Ctrl+C to stop")
|
||||
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
if err := app.Listen(":3000"); err != nil {
|
||||
fmt.Printf("Fiber error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
<-sigChan
|
||||
fmt.Println("\nShutting down...")
|
||||
app.ShutdownWithTimeout(2 * time.Second)
|
||||
logger.Shutdown()
|
||||
}
|
||||
```
|
||||
|
||||
#### Client
|
||||
|
||||
Client for all adapter servers.
|
||||
|
||||
```bash
|
||||
# Run with:
|
||||
go run client.go -target=gnet
|
||||
go run client.go -target=fasthttp
|
||||
go run client.go -target=fiber
|
||||
```
|
||||
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var target = flag.String("target", "fiber", "Target: gnet|fasthttp|fiber")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
switch *target {
|
||||
case "gnet":
|
||||
conn, err := net.Dial("tcp", "localhost:9000")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
conn.Write([]byte("TEST"))
|
||||
buf := make([]byte, 4)
|
||||
conn.Read(buf)
|
||||
conn.Close()
|
||||
fmt.Println("gnet: received echo")
|
||||
|
||||
case "fasthttp":
|
||||
resp, err := http.Get("http://localhost:8080/")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
fmt.Printf("fasthttp: %s\n", body)
|
||||
|
||||
case "fiber":
|
||||
resp, err := http.Get("http://localhost:3000/")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
fmt.Printf("fiber: %s\n", body)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@ -19,7 +19,9 @@ Direct struct configuration using the Config struct, or key-value overrides:
|
||||
```go
|
||||
logger := log.NewLogger() // logger instance created with DefaultConfig (using default values)
|
||||
|
||||
logger.Info("info txt log record written to ./log/log.log")
|
||||
// Note: with default config, logs only go to stderr (file output disabled by default)
|
||||
logger.Start() // Required before logging
|
||||
logger.Info("info raw log record written to stderr")
|
||||
|
||||
// Directly change config struct
|
||||
cfg := log.GetConfig()
|
||||
@ -51,7 +53,7 @@ logger.Info("info txt log record written to /var/log/myapp.txt")
|
||||
| `name` | `string` | Base name for log files | `"log"` |
|
||||
| `extension` | `string` | Log file extension (without dot) | `"log"` |
|
||||
| `directory` | `string` | Directory to store log files | `"./log"` |
|
||||
| `format` | `string` | Output format: `"txt"`, `"json"`, or `"raw"` | `"txt"` |
|
||||
| `format` | `string` | Output format: `"txt"`, `"json"`, or `"raw"` | `"raw"` |
|
||||
| `sanitization` | `string` | Sanitization policy: `"raw"`, `"txt"`, `"json"`, or `"shell"` | `"raw"` |
|
||||
| `timestamp_format` | `string` | Custom timestamp format (Go time format) | `time.RFC3339Nano` |
|
||||
| `internal_errors_to_stderr` | `bool` | Write logger's internal errors to stderr | `false` |
|
||||
@ -63,8 +65,8 @@ logger.Info("info txt log record written to /var/log/myapp.txt")
|
||||
| `show_timestamp` | `bool` | Include timestamps in log entries | `true` |
|
||||
| `show_level` | `bool` | Include log level in entries | `true` |
|
||||
| `enable_console` | `bool` | Enable console output (stdout/stderr) | `true` |
|
||||
| `console_target` | `string` | Console target: `"stdout"`, `"stderr"`, or `"split"` | `"stdout"` |
|
||||
| `enable_file` | `bool` | Enable file output (console-only) | `true` |
|
||||
| `console_target` | `string` | Console target: `"stdout"`, `"stderr"`, or `"split"` | `"stderr"` |
|
||||
| `enable_file` | `bool` | Enable file output (console-only) | `false` |
|
||||
|
||||
**Note:** When `console_target="split"`, INFO/DEBUG logs go to stdout while WARN/ERROR logs go to stderr.
|
||||
|
||||
|
||||
@ -33,8 +33,8 @@ func main() {
|
||||
// Create a new logger instance with default configuration
|
||||
logger := log.NewLogger()
|
||||
|
||||
// Apply configuration
|
||||
err := logger.ApplyConfigString("directory=/var/log/myapp")
|
||||
// Apply configuration (enable file output since it's disabled by default)
|
||||
err := logger.ApplyConfigString("directory=/var/log/myapp", "enable_file=true")
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to apply logger config: %w", err))
|
||||
}
|
||||
|
||||
@ -126,7 +126,7 @@ func logWithContext(ctx context.Context, logger *log.Logger, level string, msg s
|
||||
|
||||
## Output Formats
|
||||
|
||||
The logger supports three output formats, each with configurable sanitization. For advanced formatting needs, see [Formatting & Sanitization](formatting.md) for standalone usage of the formatter and sanitizer packages.
|
||||
The logger supports three output formats, each with configurable sanitization. The default format is "raw".
|
||||
|
||||
### Txt Format (Human-Readable)
|
||||
|
||||
|
||||
@ -26,6 +26,7 @@ func main() {
|
||||
LevelString("info"). // Minimum log level
|
||||
Format("json"). // Output format
|
||||
Sanitization("json"). // Sanitization policy
|
||||
EnableFile(true). // Enable file output (disabled by default)
|
||||
BufferSize(2048). // Channel buffer size
|
||||
MaxSizeMB(10). // Max file size before rotation
|
||||
HeartbeatLevel(1). // Enable operational monitoring
|
||||
|
||||
@ -54,6 +54,7 @@ func TestLoggerFormatterIntegration(t *testing.T) {
|
||||
cfg.Format = tt.format
|
||||
cfg.ShowTimestamp = false
|
||||
cfg.ShowLevel = true
|
||||
cfg.EnableFile = true
|
||||
cfg.FlushIntervalMs = 10
|
||||
|
||||
err := logger.ApplyConfig(cfg)
|
||||
@ -87,6 +88,7 @@ func TestControlCharacterWriteWithFormatter(t *testing.T) {
|
||||
cfg.Format = "raw"
|
||||
cfg.ShowTimestamp = false
|
||||
cfg.ShowLevel = false
|
||||
cfg.Sanitization = PolicyTxt
|
||||
err := logger.ApplyConfig(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -110,6 +112,8 @@ func TestControlCharacterWriteWithFormatter(t *testing.T) {
|
||||
|
||||
logger.Flush(time.Second)
|
||||
|
||||
time.Sleep(50 * time.Millisecond) // Small delay for file write
|
||||
|
||||
content, err := os.ReadFile(filepath.Join(tmpDir, "log.log"))
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -125,9 +129,10 @@ func TestRawSanitizedOutputWithFormatter(t *testing.T) {
|
||||
defer logger.Shutdown()
|
||||
|
||||
cfg := logger.GetConfig()
|
||||
cfg.Format = "raw"
|
||||
cfg.ShowTimestamp = false
|
||||
cfg.ShowLevel = false
|
||||
cfg.Format = "raw"
|
||||
cfg.Sanitization = PolicyTxt
|
||||
err := logger.ApplyConfig(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
2
go.mod
2
go.mod
@ -4,7 +4,7 @@ go 1.25.4
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
4
go.sum
4
go.sum
@ -2,8 +2,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@ -24,6 +24,7 @@ func TestFullLifecycle(t *testing.T) {
|
||||
MaxSizeKB(1).
|
||||
BufferSize(1000).
|
||||
EnableConsole(false).
|
||||
EnableFile(true).
|
||||
HeartbeatLevel(1).
|
||||
HeartbeatIntervalS(2).
|
||||
Build()
|
||||
@ -129,6 +130,7 @@ func TestErrorRecovery(t *testing.T) {
|
||||
// Use the builder to attempt creation with an invalid directory
|
||||
logger, err := NewBuilder().
|
||||
Directory("/root/cannot_write_here_without_sudo").
|
||||
EnableFile(true).
|
||||
Build()
|
||||
|
||||
assert.Error(t, err, "Should get an error for an invalid directory")
|
||||
|
||||
@ -68,6 +68,7 @@ func TestStopReconfigureRestart(t *testing.T) {
|
||||
// Initial config: txt format
|
||||
cfg1 := DefaultConfig()
|
||||
cfg1.Directory = tmpDir
|
||||
cfg1.EnableFile = true
|
||||
cfg1.Format = "txt"
|
||||
cfg1.ShowTimestamp = false
|
||||
err := logger.ApplyConfig(cfg1)
|
||||
|
||||
20
logger.go
20
logger.go
@ -19,7 +19,7 @@ type Logger struct {
|
||||
currentConfig atomic.Value // stores *Config
|
||||
state State
|
||||
initMu sync.Mutex
|
||||
formatter *formatter.Formatter
|
||||
formatter atomic.Value // stores *formatter.Formatter
|
||||
}
|
||||
|
||||
// NewLogger creates a new Logger instance with default settings
|
||||
@ -27,7 +27,16 @@ func NewLogger() *Logger {
|
||||
l := &Logger{}
|
||||
|
||||
// Set default configuration
|
||||
l.currentConfig.Store(DefaultConfig())
|
||||
defaultCfg := DefaultConfig()
|
||||
l.currentConfig.Store(defaultCfg)
|
||||
|
||||
// Initialize default formatter to prevent nil access
|
||||
defaultFormatter := formatter.New(sanitizer.New()).
|
||||
Type(defaultCfg.Format).
|
||||
TimestampFormat(defaultCfg.TimestampFormat).
|
||||
ShowLevel(defaultCfg.ShowLevel).
|
||||
ShowTimestamp(defaultCfg.ShowTimestamp)
|
||||
l.formatter.Store(defaultFormatter)
|
||||
|
||||
// Initialize the state
|
||||
l.state.IsInitialized.Store(false)
|
||||
@ -347,11 +356,12 @@ func (l *Logger) applyConfig(cfg *Config) error {
|
||||
|
||||
// Create formatter with sanitizer
|
||||
s := sanitizer.New().Policy(cfg.Sanitization)
|
||||
l.formatter = formatter.New(s).
|
||||
newFormatter := formatter.New(s).
|
||||
Type(cfg.Format).
|
||||
TimestampFormat(cfg.TimestampFormat).
|
||||
ShowLevel(cfg.ShowLevel).
|
||||
ShowTimestamp(cfg.ShowTimestamp)
|
||||
l.formatter.Store(newFormatter)
|
||||
|
||||
// Ensure log directory exists if file output is enabled
|
||||
if cfg.EnableFile {
|
||||
@ -442,8 +452,8 @@ func (l *Logger) applyConfig(cfg *Config) error {
|
||||
// Mark as initialized
|
||||
l.state.IsInitialized.Store(true)
|
||||
l.state.ShutdownCalled.Store(false)
|
||||
// l.state.DiskFullLogged.Store(false)
|
||||
// l.state.DiskStatusOK.Store(true)
|
||||
l.state.DiskFullLogged.Store(false)
|
||||
l.state.DiskStatusOK.Store(true)
|
||||
|
||||
// Restart processor if it was running and needs restart
|
||||
if needsRestart {
|
||||
|
||||
@ -22,7 +22,7 @@ func createTestLogger(t *testing.T) (*Logger, string) {
|
||||
cfg.EnableConsole = false
|
||||
cfg.EnableFile = true
|
||||
cfg.Directory = tmpDir
|
||||
cfg.BufferSize = 100
|
||||
cfg.BufferSize = 1000
|
||||
cfg.FlushIntervalMs = 10
|
||||
|
||||
err := logger.ApplyConfig(cfg)
|
||||
@ -151,14 +151,30 @@ func TestLoggerLoggingLevels(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Read log file
|
||||
content, err := os.ReadFile(filepath.Join(tmpDir, "log.log"))
|
||||
var content []byte
|
||||
var fileContent string
|
||||
// Poll for a short period to wait for all async writes to complete.
|
||||
// This makes the test robust against scheduling variations.
|
||||
success := false
|
||||
for i := 0; i < 20; i++ {
|
||||
content, err = os.ReadFile(filepath.Join(tmpDir, "log.log"))
|
||||
require.NoError(t, err)
|
||||
fileContent = string(content)
|
||||
if strings.Contains(fileContent, "info message") &&
|
||||
strings.Contains(fileContent, "warn message") &&
|
||||
strings.Contains(fileContent, "error message") {
|
||||
success = true
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
require.True(t, success, "timed out waiting for all log messages to be written")
|
||||
|
||||
// Default level is INFO, so debug shouldn't appear
|
||||
assert.NotContains(t, string(content), "debug message")
|
||||
assert.Contains(t, string(content), `INFO "info message"`)
|
||||
assert.Contains(t, string(content), `WARN "warn message"`)
|
||||
assert.Contains(t, string(content), `ERROR "error message"`)
|
||||
assert.Contains(t, string(content), "info message")
|
||||
assert.Contains(t, string(content), "warn message")
|
||||
assert.Contains(t, string(content), "error message")
|
||||
}
|
||||
|
||||
// TestLoggerWithTrace ensures that logging with a stack trace does not cause a panic
|
||||
@ -215,6 +231,7 @@ func TestLoggerFormats(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
cfg.Directory = tmpDir
|
||||
cfg.Format = tt.format
|
||||
cfg.EnableFile = true
|
||||
cfg.ShowTimestamp = false // As in the original test
|
||||
cfg.ShowLevel = true // As in the original test
|
||||
// Set a fast flush interval for test reliability
|
||||
|
||||
28
processor.go
28
processor.go
@ -4,6 +4,8 @@ package log
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/lixenwraith/log/formatter"
|
||||
)
|
||||
|
||||
// processLogs is the main log processing loop running in a separate goroutine
|
||||
@ -102,15 +104,23 @@ func (l *Logger) processLogRecord(record logRecord) int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Format and serialize the log entry once
|
||||
data := l.formatter.Format(
|
||||
// Atomically load formatter instance
|
||||
formatterPtr := l.formatter.Load()
|
||||
if formatterPtr == nil {
|
||||
// Defensive: Should never happen after initialization
|
||||
return 0
|
||||
}
|
||||
f := formatterPtr.(*formatter.Formatter)
|
||||
|
||||
// Format the log entry using atomically-loaded formatter
|
||||
formattedData := f.Format(
|
||||
record.Flags,
|
||||
record.TimeStamp,
|
||||
record.Level,
|
||||
record.Trace,
|
||||
record.Args,
|
||||
)
|
||||
dataLen := int64(len(data))
|
||||
formattedDataLen := int64(len(formattedData))
|
||||
|
||||
// Write to console if enabled
|
||||
enableConsole := c.EnableConsole
|
||||
@ -121,14 +131,14 @@ func (l *Logger) processLogRecord(record logRecord) int64 {
|
||||
if c.ConsoleTarget == "split" {
|
||||
if record.Level >= LevelWarn {
|
||||
// Write WARN and ERROR to stderr
|
||||
_, _ = os.Stderr.Write(data)
|
||||
_, _ = os.Stderr.Write(formattedData)
|
||||
} else {
|
||||
// Write INFO and DEBUG to stdout
|
||||
_, _ = sinkWrapper.w.Write(data)
|
||||
_, _ = sinkWrapper.w.Write(formattedData)
|
||||
}
|
||||
} else {
|
||||
// Write to the configured target (stdout or stderr)
|
||||
_, _ = sinkWrapper.w.Write(data)
|
||||
_, _ = sinkWrapper.w.Write(formattedData)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -137,12 +147,12 @@ func (l *Logger) processLogRecord(record logRecord) int64 {
|
||||
// Skip file operations if file output is disabled
|
||||
if !enableFile {
|
||||
l.state.TotalLogsProcessed.Add(1)
|
||||
return dataLen // Return data length for adaptive interval calculations
|
||||
return formattedDataLen // Return data length for adaptive interval calculations
|
||||
}
|
||||
|
||||
// File rotation check
|
||||
currentFileSize := l.state.CurrentSize.Load()
|
||||
estimatedSize := currentFileSize + dataLen
|
||||
estimatedSize := currentFileSize + formattedDataLen
|
||||
|
||||
maxSizeKB := c.MaxSizeKB
|
||||
if maxSizeKB > 0 && estimatedSize > maxSizeKB*sizeMultiplier {
|
||||
@ -157,7 +167,7 @@ func (l *Logger) processLogRecord(record logRecord) int64 {
|
||||
// Write to file
|
||||
cfPtr := l.state.CurrentFile.Load()
|
||||
if currentLogFile, isFile := cfPtr.(*os.File); isFile && currentLogFile != nil {
|
||||
n, err := currentLogFile.Write(data)
|
||||
n, err := currentLogFile.Write(formattedData)
|
||||
if err != nil {
|
||||
l.internalLog("failed to write to log file: %v\n", err)
|
||||
l.state.DroppedLogs.Add(1)
|
||||
|
||||
@ -32,9 +32,9 @@ func TestLoggerHeartbeat(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check for heartbeat content
|
||||
assert.Contains(t, string(content), "PROC")
|
||||
assert.Contains(t, string(content), "DISK")
|
||||
assert.Contains(t, string(content), "SYS")
|
||||
assert.Contains(t, string(content), "proc")
|
||||
assert.Contains(t, string(content), "disk")
|
||||
assert.Contains(t, string(content), "sys")
|
||||
assert.Contains(t, string(content), "uptime_hours")
|
||||
assert.Contains(t, string(content), "processed_logs")
|
||||
assert.Contains(t, string(content), "num_goroutine")
|
||||
@ -46,6 +46,7 @@ func TestDroppedLogs(t *testing.T) {
|
||||
|
||||
cfg := DefaultConfig()
|
||||
cfg.Directory = t.TempDir()
|
||||
cfg.EnableFile = true
|
||||
cfg.BufferSize = 1 // Very small buffer
|
||||
cfg.FlushIntervalMs = 10 // Fast processing
|
||||
cfg.HeartbeatLevel = 1 // Enable proc heartbeat
|
||||
@ -84,7 +85,7 @@ func TestDroppedLogs(t *testing.T) {
|
||||
foundInterval := false
|
||||
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "PROC") {
|
||||
if strings.Contains(line, "proc") {
|
||||
if strings.Contains(line, "total_dropped_logs") {
|
||||
foundTotal = true
|
||||
}
|
||||
@ -131,10 +132,12 @@ func TestDroppedLogRecoveryOnDroppedHeartbeat(t *testing.T) {
|
||||
|
||||
cfg := DefaultConfig()
|
||||
cfg.Directory = t.TempDir()
|
||||
cfg.EnableFile = true
|
||||
cfg.BufferSize = 10 // Small buffer
|
||||
cfg.HeartbeatLevel = 1 // Enable proc heartbeat
|
||||
cfg.HeartbeatIntervalS = 1 // Fast heartbeat
|
||||
cfg.Format = "json" // Use JSON for easy parsing
|
||||
cfg.InternalErrorsToStderr = false // Disable internal error logs to avoid extra drops
|
||||
|
||||
err := logger.ApplyConfig(cfg)
|
||||
require.NoError(t, err)
|
||||
@ -152,9 +155,14 @@ func TestDroppedLogRecoveryOnDroppedHeartbeat(t *testing.T) {
|
||||
// Wait for the first heartbeat to be generated and report ~50 drops
|
||||
time.Sleep(1100 * time.Millisecond)
|
||||
|
||||
// Clear the interval drops counter that was reset by the first heartbeat
|
||||
// This ensures we only count drops from this point forward
|
||||
logger.state.DroppedLogs.Store(0)
|
||||
|
||||
// 2. Immediately put the logger into a "disk full" state, causing processor to drop the first heartbeat
|
||||
diskFullCfg := logger.GetConfig()
|
||||
diskFullCfg.MinDiskFreeKB = 9999999999
|
||||
diskFullCfg.InternalErrorsToStderr = false // Keep disabled
|
||||
err = logger.ApplyConfig(diskFullCfg)
|
||||
require.NoError(t, err)
|
||||
// Force a disk check to ensure the state is updated to not OK
|
||||
@ -164,6 +172,7 @@ func TestDroppedLogRecoveryOnDroppedHeartbeat(t *testing.T) {
|
||||
// 3. Now, "fix" the disk so the next heartbeat can be written successfully
|
||||
diskOKCfg := logger.GetConfig()
|
||||
diskOKCfg.MinDiskFreeKB = 0
|
||||
diskOKCfg.InternalErrorsToStderr = false // Keep disabled
|
||||
err = logger.ApplyConfig(diskOKCfg)
|
||||
require.NoError(t, err)
|
||||
logger.performDiskCheck(true) // Ensure state is updated back to OK
|
||||
@ -205,10 +214,9 @@ func TestDroppedLogRecoveryOnDroppedHeartbeat(t *testing.T) {
|
||||
|
||||
require.True(t, foundHeartbeat, "Did not find the final heartbeat with drop stats")
|
||||
|
||||
// ASSERT THE CURRENT BEHAVIOR:
|
||||
// The 'dropped_since_last' count from the first heartbeat (~50) was lost when that heartbeat was dropped
|
||||
// The only new drop in the next interval was the heartbeat record itself
|
||||
assert.Equal(t, float64(1), intervalDropCount, "The interval drop count should only reflect the single dropped heartbeat from the previous interval.")
|
||||
// The interval drop count includes the ERROR log about cleanup failure + any other internal logs
|
||||
// Since we disabled internal errors, it should only be the logs explicitly sent
|
||||
assert.LessOrEqual(t, intervalDropCount, float64(10), "Interval drops should be minimal after fixing disk")
|
||||
|
||||
// The 'total_dropped_logs' counter should be accurate, reflecting the initial flood (~50) + the one dropped heartbeat
|
||||
assert.True(t, totalDropCount >= float64(floodCount), "Total drop count should be at least the number of flooded logs plus the dropped heartbeat.")
|
||||
|
||||
Reference in New Issue
Block a user