e1.11.0 Configuration refactored and simplified (interface changed).
This commit is contained in:
@ -2,71 +2,144 @@
|
|||||||
package compat
|
package compat
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"github.com/lixenwraith/log"
|
"github.com/lixenwraith/log"
|
||||||
"github.com/panjf2000/gnet/v2"
|
|
||||||
"github.com/valyala/fasthttp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Builder provides a convenient way to create configured loggers for both frameworks
|
// Builder provides a flexible way to create configured logger adapters for gnet and fasthttp.
|
||||||
|
// It can use an existing *log.Logger instance or create a new one from a *log.Config.
|
||||||
type Builder struct {
|
type Builder struct {
|
||||||
logger *log.Logger
|
logger *log.Logger
|
||||||
options []string // InitWithDefaults options
|
logCfg *log.Config
|
||||||
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBuilder creates a new adapter builder
|
// NewBuilder creates a new adapter builder.
|
||||||
func NewBuilder() *Builder {
|
func NewBuilder() *Builder {
|
||||||
return &Builder{
|
return &Builder{}
|
||||||
logger: log.NewLogger(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithOptions adds configuration options for the underlying logger
|
// WithLogger specifies an existing logger to use for the adapters. This is the recommended
|
||||||
func (b *Builder) WithOptions(opts ...string) *Builder {
|
// approach for applications that already have a central logger instance.
|
||||||
b.options = append(b.options, opts...)
|
// If this is set, any configuration passed via WithConfig is ignored.
|
||||||
|
func (b *Builder) WithLogger(l *log.Logger) *Builder {
|
||||||
|
if l == nil {
|
||||||
|
b.err = fmt.Errorf("log/compat: provided logger cannot be nil")
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
b.logger = l
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build initializes the logger and returns adapters for both frameworks
|
// WithConfig provides a configuration for a new logger instance.
|
||||||
func (b *Builder) Build() (*GnetAdapter, *FastHTTPAdapter, error) {
|
// This is used only if an existing logger is NOT provided via WithLogger.
|
||||||
// Initialize the logger
|
// If neither WithLogger nor WithConfig is used, a default logger will be created.
|
||||||
if err := b.logger.InitWithDefaults(b.options...); err != nil {
|
func (b *Builder) WithConfig(cfg *log.Config) *Builder {
|
||||||
return nil, nil, err
|
b.logCfg = cfg
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLogger resolves the logger to be used, creating one if necessary.
|
||||||
|
// It's called internally by the build methods.
|
||||||
|
func (b *Builder) getLogger() (*log.Logger, error) {
|
||||||
|
if b.err != nil {
|
||||||
|
return nil, b.err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create adapters
|
// An existing logger was provided, so we use it.
|
||||||
gnetAdapter := NewGnetAdapter(b.logger)
|
if b.logger != nil {
|
||||||
fasthttpAdapter := NewFastHTTPAdapter(b.logger)
|
return b.logger, nil
|
||||||
|
|
||||||
return gnetAdapter, fasthttpAdapter, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildStructured initializes the logger and returns structured adapters
|
|
||||||
func (b *Builder) BuildStructured() (*StructuredGnetAdapter, *FastHTTPAdapter, error) {
|
|
||||||
// Initialize the logger
|
|
||||||
if err := b.logger.InitWithDefaults(b.options...); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create adapters
|
// Create a new logger instance.
|
||||||
gnetAdapter := NewStructuredGnetAdapter(b.logger)
|
l := log.NewLogger()
|
||||||
fasthttpAdapter := NewFastHTTPAdapter(b.logger)
|
cfg := b.logCfg
|
||||||
|
if cfg == nil {
|
||||||
|
// If no config was provided, use the default.
|
||||||
|
cfg = log.DefaultConfig()
|
||||||
|
}
|
||||||
|
|
||||||
return gnetAdapter, fasthttpAdapter, nil
|
// Apply the configuration.
|
||||||
|
if err := l.ApplyConfig(cfg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the newly created logger for subsequent builds with this builder.
|
||||||
|
b.logger = l
|
||||||
|
return l, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLogger returns the underlying logger for direct access
|
// BuildGnet creates a gnet adapter.
|
||||||
func (b *Builder) GetLogger() *log.Logger {
|
// It can be used for servers that require a standard gnet logger.
|
||||||
return b.logger
|
func (b *Builder) BuildGnet(opts ...GnetOption) (*GnetAdapter, error) {
|
||||||
|
l, err := b.getLogger()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewGnetAdapter(l, opts...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example usage functions
|
// BuildStructuredGnet creates a gnet adapter that attempts to extract structured
|
||||||
|
// fields from log messages for richer, queryable logs.
|
||||||
// ConfigureGnetServer configures a gnet server with the logger
|
func (b *Builder) BuildStructuredGnet(opts ...GnetOption) (*StructuredGnetAdapter, error) {
|
||||||
func ConfigureGnetServer(adapter *GnetAdapter, opts ...gnet.Option) []gnet.Option {
|
l, err := b.getLogger()
|
||||||
return append(opts, gnet.WithLogger(adapter))
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewStructuredGnetAdapter(l, opts...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigureFastHTTPServer configures a fasthttp server with the logger
|
// BuildFastHTTP creates a fasthttp adapter.
|
||||||
func ConfigureFastHTTPServer(adapter *FastHTTPAdapter, server *fasthttp.Server) {
|
func (b *Builder) BuildFastHTTP(opts ...FastHTTPOption) (*FastHTTPAdapter, error) {
|
||||||
server.Logger = adapter
|
l, err := b.getLogger()
|
||||||
}
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewFastHTTPAdapter(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) {
|
||||||
|
return b.getLogger()
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Example Usage ---
|
||||||
|
//
|
||||||
|
// The following demonstrates how to integrate lixenwraith/log with gnet and fasthttp
|
||||||
|
// using a single, shared logger instance.
|
||||||
|
//
|
||||||
|
// // 1. Create and configure your application's main logger.
|
||||||
|
// appLogger := log.NewLogger()
|
||||||
|
// logCfg := log.DefaultConfig()
|
||||||
|
// logCfg.Level = log.LevelDebug
|
||||||
|
// if err := appLogger.ApplyConfig(logCfg); err != nil {
|
||||||
|
// panic(fmt.Sprintf("failed to configure logger: %v", err))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // 2. Create a builder and provide the existing logger.
|
||||||
|
// builder := compat.NewBuilder().WithLogger(appLogger)
|
||||||
|
//
|
||||||
|
// // 3. Build the required adapters.
|
||||||
|
// gnetLogger, err := builder.BuildGnet()
|
||||||
|
// if err != nil { /* handle error */ }
|
||||||
|
//
|
||||||
|
// fasthttpLogger, err := builder.BuildFastHTTP()
|
||||||
|
// if err != nil { /* handle error */ }
|
||||||
|
//
|
||||||
|
// // 4. Configure your servers with the adapters.
|
||||||
|
//
|
||||||
|
// // For gnet:
|
||||||
|
// var events gnet.EventHandler // your-event-handler
|
||||||
|
// // The adapter is passed directly into the gnet options.
|
||||||
|
// go gnet.Run(events, "tcp://:9000", gnet.WithLogger(gnetLogger))
|
||||||
|
//
|
||||||
|
// // For fasthttp:
|
||||||
|
// // The adapter is assigned directly to the server's Logger field.
|
||||||
|
// server := &fasthttp.Server{
|
||||||
|
// Handler: func(ctx *fasthttp.RequestCtx) {
|
||||||
|
// ctx.WriteString("Hello, world!")
|
||||||
|
// },
|
||||||
|
// Logger: fasthttpLogger,
|
||||||
|
// }
|
||||||
|
// go server.ListenAndServe(":8080")
|
||||||
174
config.go
174
config.go
@ -2,10 +2,6 @@
|
|||||||
package log
|
package log
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/lixenwraith/config"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -105,161 +101,17 @@ var defaultConfig = Config{
|
|||||||
// DefaultConfig returns a copy of the default configuration
|
// DefaultConfig returns a copy of the default configuration
|
||||||
func DefaultConfig() *Config {
|
func DefaultConfig() *Config {
|
||||||
// Create a copy to prevent modifications to the original
|
// Create a copy to prevent modifications to the original
|
||||||
copiedConfig := defaultConfig
|
return defaultConfig.Clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone creates a deep copy of the configuration
|
||||||
|
func (c *Config) Clone() *Config {
|
||||||
|
copiedConfig := *c
|
||||||
return &copiedConfig
|
return &copiedConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConfigFromFile loads configuration from a TOML file and returns a validated Config
|
// Validate performs validation on the configuration
|
||||||
func NewConfigFromFile(path string) (*Config, error) {
|
func (c *Config) Validate() error {
|
||||||
cfg := DefaultConfig()
|
|
||||||
|
|
||||||
// Use lixenwraith/config as a loader
|
|
||||||
loader := config.New()
|
|
||||||
|
|
||||||
// Register the struct to enable proper unmarshaling
|
|
||||||
if err := loader.RegisterStruct("log.", *cfg); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to register config struct: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load from file (handles file not found gracefully)
|
|
||||||
if err := loader.Load(path, nil); err != nil && !errors.Is(err, config.ErrConfigNotFound) {
|
|
||||||
return nil, fmt.Errorf("failed to load config from %s: %w", path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract values into our Config struct
|
|
||||||
if err := extractConfig(loader, "log.", cfg); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to extract config values: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the loaded configuration
|
|
||||||
if err := cfg.validate(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewConfigFromDefaults creates a Config with default values and applies overrides
|
|
||||||
func NewConfigFromDefaults(overrides map[string]any) (*Config, error) {
|
|
||||||
cfg := DefaultConfig()
|
|
||||||
|
|
||||||
// Apply overrides using reflection
|
|
||||||
if err := applyOverrides(cfg, overrides); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to apply overrides: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the configuration
|
|
||||||
if err := cfg.validate(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractConfig extracts values from lixenwraith/config into our Config struct
|
|
||||||
func extractConfig(loader *config.Config, prefix string, cfg *Config) error {
|
|
||||||
v := reflect.ValueOf(cfg).Elem()
|
|
||||||
t := v.Type()
|
|
||||||
|
|
||||||
for i := 0; i < t.NumField(); i++ {
|
|
||||||
field := t.Field(i)
|
|
||||||
fieldValue := v.Field(i)
|
|
||||||
|
|
||||||
// Get the toml tag to determine the config key
|
|
||||||
tomlTag := field.Tag.Get("toml")
|
|
||||||
if tomlTag == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
key := prefix + tomlTag
|
|
||||||
|
|
||||||
// Get value from loader
|
|
||||||
val, found := loader.Get(key)
|
|
||||||
if !found {
|
|
||||||
continue // Use default value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the field value with type conversion
|
|
||||||
if err := setFieldValue(fieldValue, val); err != nil {
|
|
||||||
return fmt.Errorf("failed to set field %s: %w", field.Name, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// applyOverrides applies a map of overrides to the Config struct
|
|
||||||
func applyOverrides(cfg *Config, overrides map[string]any) error {
|
|
||||||
v := reflect.ValueOf(cfg).Elem()
|
|
||||||
t := v.Type()
|
|
||||||
|
|
||||||
// Create a map of field names to field values for efficient lookup
|
|
||||||
fieldMap := make(map[string]reflect.Value)
|
|
||||||
for i := 0; i < t.NumField(); i++ {
|
|
||||||
field := t.Field(i)
|
|
||||||
tomlTag := field.Tag.Get("toml")
|
|
||||||
if tomlTag != "" {
|
|
||||||
fieldMap[tomlTag] = v.Field(i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, value := range overrides {
|
|
||||||
fieldValue, exists := fieldMap[key]
|
|
||||||
if !exists {
|
|
||||||
return fmt.Errorf("unknown config key: %s", key)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := setFieldValue(fieldValue, value); err != nil {
|
|
||||||
return fmt.Errorf("failed to set %s: %w", key, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// setFieldValue sets a reflect.Value with proper type conversion
|
|
||||||
func setFieldValue(field reflect.Value, value any) error {
|
|
||||||
switch field.Kind() {
|
|
||||||
case reflect.String:
|
|
||||||
strVal, ok := value.(string)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("expected string, got %T", value)
|
|
||||||
}
|
|
||||||
field.SetString(strVal)
|
|
||||||
|
|
||||||
case reflect.Int64:
|
|
||||||
switch v := value.(type) {
|
|
||||||
case int64:
|
|
||||||
field.SetInt(v)
|
|
||||||
case int:
|
|
||||||
field.SetInt(int64(v))
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("expected int64, got %T", value)
|
|
||||||
}
|
|
||||||
|
|
||||||
case reflect.Float64:
|
|
||||||
floatVal, ok := value.(float64)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("expected float64, got %T", value)
|
|
||||||
}
|
|
||||||
field.SetFloat(floatVal)
|
|
||||||
|
|
||||||
case reflect.Bool:
|
|
||||||
boolVal, ok := value.(bool)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("expected bool, got %T", value)
|
|
||||||
}
|
|
||||||
field.SetBool(boolVal)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported field type: %v", field.Kind())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate performs validation on the configuration
|
|
||||||
func (c *Config) validate() error {
|
|
||||||
// String validations
|
// String validations
|
||||||
if strings.TrimSpace(c.Name) == "" {
|
if strings.TrimSpace(c.Name) == "" {
|
||||||
return fmtErrorf("log name cannot be empty")
|
return fmtErrorf("log name cannot be empty")
|
||||||
@ -277,8 +129,8 @@ func (c *Config) validate() error {
|
|||||||
return fmtErrorf("timestamp_format cannot be empty")
|
return fmtErrorf("timestamp_format cannot be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.StdoutTarget != "stdout" && c.StdoutTarget != "stderr" {
|
if c.StdoutTarget != "stdout" && c.StdoutTarget != "stderr" && c.StdoutTarget != "split" {
|
||||||
return fmtErrorf("invalid stdout_target: '%s' (use stdout or stderr)", c.StdoutTarget)
|
return fmtErrorf("invalid stdout_target: '%s' (use stdout, stderr, or split)", c.StdoutTarget)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Numeric validations
|
// Numeric validations
|
||||||
@ -319,10 +171,4 @@ func (c *Config) validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
// Clone creates a deep copy of the configuration
|
|
||||||
func (c *Config) Clone() *Config {
|
|
||||||
copiedConfig := *c
|
|
||||||
return &copiedConfig
|
|
||||||
}
|
}
|
||||||
22
format.go
22
format.go
@ -364,24 +364,18 @@ func (s *serializer) serializeStructuredJSON(flags int64, timestamp time.Time, l
|
|||||||
s.buf = append(s.buf, `"message":"`...)
|
s.buf = append(s.buf, `"message":"`...)
|
||||||
s.writeString(message)
|
s.writeString(message)
|
||||||
s.buf = append(s.buf, '"')
|
s.buf = append(s.buf, '"')
|
||||||
needsComma = true
|
|
||||||
|
|
||||||
// // Add trace if present
|
// Add trace if present
|
||||||
// if trace != "" {
|
if trace != "" {
|
||||||
// if needsComma {
|
s.buf = append(s.buf, ',')
|
||||||
// s.buf = append(s.buf, ',')
|
s.buf = append(s.buf, `"trace":"`...)
|
||||||
// }
|
s.writeString(trace)
|
||||||
// s.buf = append(s.buf, `"trace":"`...)
|
s.buf = append(s.buf, '"')
|
||||||
// s.writeString(trace)
|
}
|
||||||
// s.buf = append(s.buf, '"')
|
|
||||||
// needsComma = true
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Marshal fields using encoding/json
|
// Marshal fields using encoding/json
|
||||||
if len(fields) > 0 {
|
if len(fields) > 0 {
|
||||||
if needsComma {
|
s.buf = append(s.buf, ',')
|
||||||
s.buf = append(s.buf, ',')
|
|
||||||
}
|
|
||||||
s.buf = append(s.buf, `"fields":`...)
|
s.buf = append(s.buf, `"fields":`...)
|
||||||
|
|
||||||
// Use json.Marshal for proper encoding
|
// Use json.Marshal for proper encoding
|
||||||
|
|||||||
4
go.mod
4
go.mod
@ -4,9 +4,9 @@ go 1.24.5
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1
|
github.com/davecgh/go-spew v1.1.1
|
||||||
github.com/lixenwraith/config v0.0.0-20250712170030-7d38402e0497
|
github.com/lixenwraith/config v0.0.0-20250715165746-b26e47c0c757
|
||||||
github.com/panjf2000/gnet/v2 v2.9.1
|
github.com/panjf2000/gnet/v2 v2.9.1
|
||||||
github.com/valyala/fasthttp v1.63.0
|
github.com/valyala/fasthttp v1.64.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|||||||
8
go.sum
8
go.sum
@ -6,8 +6,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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/lixenwraith/config v0.0.0-20250712170030-7d38402e0497 h1:ixTIdJSd945n/IhMRwGwQVmQnQ1nUr5z1wn31jXq9FU=
|
github.com/lixenwraith/config v0.0.0-20250715165746-b26e47c0c757 h1:VTopw1oA7XijJa+5ZTneVLZGD4LPmUHITdqaCckfI78=
|
||||||
github.com/lixenwraith/config v0.0.0-20250712170030-7d38402e0497/go.mod h1:y7kgDrWIFROWJJ6ASM/SPTRRAj27FjRGWh2SDLcdQ68=
|
github.com/lixenwraith/config v0.0.0-20250715165746-b26e47c0c757/go.mod h1:y7kgDrWIFROWJJ6ASM/SPTRRAj27FjRGWh2SDLcdQ68=
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg=
|
github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg=
|
||||||
@ -20,8 +20,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
|
|||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasthttp v1.63.0 h1:DisIL8OjB7ul2d7cBaMRcKTQDYnrGy56R4FCiuDP0Ns=
|
github.com/valyala/fasthttp v1.64.0 h1:QBygLLQmiAyiXuRhthf0tuRkqAFcrC42dckN2S+N3og=
|
||||||
github.com/valyala/fasthttp v1.63.0/go.mod h1:REc4IeW+cAEyLrRPa5A81MIjvz0QE1laoTX2EaPHKJM=
|
github.com/valyala/fasthttp v1.64.0/go.mod h1:dGmFxwkWXSK0NbOSJuF7AMVzU+lkHz0wQVvVITv2UQA=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
|||||||
35
logger.go
35
logger.go
@ -9,8 +9,6 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/lixenwraith/config"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Logger is the core struct that encapsulates all logger functionality
|
// Logger is the core struct that encapsulates all logger functionality
|
||||||
@ -57,16 +55,16 @@ func NewLogger() *Logger {
|
|||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
// getConfig returns the current configuration (thread-safe)
|
// ApplyConfig applies a validated configuration to the logger
|
||||||
func (l *Logger) getConfig() *Config {
|
// This is the primary way applications should configure the logger
|
||||||
return l.currentConfig.Load().(*Config)
|
func (l *Logger) ApplyConfig(cfg *Config) error {
|
||||||
}
|
if cfg == nil {
|
||||||
|
return fmt.Errorf("log: configuration cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
// LoadConfig loads logger configuration from a file
|
// Validate the configuration
|
||||||
func (l *Logger) LoadConfig(path string) error {
|
if err := cfg.Validate(); err != nil {
|
||||||
cfg, err := NewConfigFromFile(path)
|
return fmt.Errorf("log: invalid configuration: %w", err)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
l.initMu.Lock()
|
l.initMu.Lock()
|
||||||
@ -75,18 +73,9 @@ func (l *Logger) LoadConfig(path string) error {
|
|||||||
return l.apply(cfg)
|
return l.apply(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveConfig saves the current logger configuration to a file
|
// getConfig returns the current configuration (thread-safe)
|
||||||
func (l *Logger) SaveConfig(path string) error {
|
func (l *Logger) getConfig() *Config {
|
||||||
// Create a lixenwraith/config instance for saving
|
return l.currentConfig.Load().(*Config)
|
||||||
saver := config.New()
|
|
||||||
cfg := l.getConfig()
|
|
||||||
|
|
||||||
// Register all fields with their current values
|
|
||||||
if err := saver.RegisterStruct("log.", *cfg); err != nil {
|
|
||||||
return fmt.Errorf("failed to register config for saving: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return saver.Save(path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// apply applies a validated configuration and reconfigures logger components
|
// apply applies a validated configuration and reconfigures logger components
|
||||||
|
|||||||
20
processor.go
20
processor.go
@ -15,7 +15,7 @@ const (
|
|||||||
adaptiveIntervalFactor float64 = 1.5 // Slow down
|
adaptiveIntervalFactor float64 = 1.5 // Slow down
|
||||||
adaptiveSpeedUpFactor float64 = 0.8 // Speed up
|
adaptiveSpeedUpFactor float64 = 0.8 // Speed up
|
||||||
// Minimum wait time used throughout the package
|
// Minimum wait time used throughout the package
|
||||||
minWaitTime = time.Duration(10 * time.Millisecond)
|
minWaitTime = 10 * time.Millisecond
|
||||||
)
|
)
|
||||||
|
|
||||||
// processLogs is the main log processing loop running in a separate goroutine
|
// processLogs is the main log processing loop running in a separate goroutine
|
||||||
@ -50,7 +50,7 @@ func (l *Logger) processLogs(ch <-chan logRecord) {
|
|||||||
|
|
||||||
// State variables for adaptive disk checks
|
// State variables for adaptive disk checks
|
||||||
var bytesSinceLastCheck int64 = 0
|
var bytesSinceLastCheck int64 = 0
|
||||||
var lastCheckTime time.Time = time.Now()
|
var lastCheckTime = time.Now()
|
||||||
var logsSinceLastCheck int64 = 0
|
var logsSinceLastCheck int64 = 0
|
||||||
|
|
||||||
// --- Main Loop ---
|
// --- Main Loop ---
|
||||||
@ -235,10 +235,20 @@ func (l *Logger) processLogRecord(record logRecord) int64 {
|
|||||||
enableStdout := c.EnableStdout
|
enableStdout := c.EnableStdout
|
||||||
if enableStdout {
|
if enableStdout {
|
||||||
if s := l.state.StdoutWriter.Load(); s != nil {
|
if s := l.state.StdoutWriter.Load(); s != nil {
|
||||||
// Assert to concrete type: *sink
|
|
||||||
if sinkWrapper, ok := s.(*sink); ok && sinkWrapper != nil {
|
if sinkWrapper, ok := s.(*sink); ok && sinkWrapper != nil {
|
||||||
// Use the wrapped writer (sinkWrapper.w)
|
// Handle split mode
|
||||||
_, _ = sinkWrapper.w.Write(data)
|
if c.StdoutTarget == "split" {
|
||||||
|
if record.Level >= LevelWarn {
|
||||||
|
// Write WARN and ERROR to stderr
|
||||||
|
_, _ = os.Stderr.Write(data)
|
||||||
|
} else {
|
||||||
|
// Write INFO and DEBUG to stdout
|
||||||
|
_, _ = sinkWrapper.w.Write(data)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Write to the configured target (stdout or stderr)
|
||||||
|
_, _ = sinkWrapper.w.Write(data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
93
state.go
93
state.go
@ -2,12 +2,8 @@
|
|||||||
package log
|
package log
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
@ -46,95 +42,6 @@ type sink struct {
|
|||||||
w io.Writer
|
w io.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes the logger using a map of configuration values
|
|
||||||
func (l *Logger) Init(values map[string]any) error {
|
|
||||||
cfg, err := NewConfigFromDefaults(values)
|
|
||||||
if err != nil {
|
|
||||||
l.state.LoggerDisabled.Store(true)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
l.initMu.Lock()
|
|
||||||
defer l.initMu.Unlock()
|
|
||||||
|
|
||||||
if l.state.LoggerDisabled.Load() {
|
|
||||||
return fmtErrorf("logger previously failed to initialize and is disabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
return l.apply(cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitWithDefaults initializes the logger with built-in defaults and optional overrides
|
|
||||||
func (l *Logger) InitWithDefaults(overrides ...string) error {
|
|
||||||
// Parse overrides into a map
|
|
||||||
overrideMap := make(map[string]any)
|
|
||||||
|
|
||||||
defaults := DefaultConfig()
|
|
||||||
for _, override := range overrides {
|
|
||||||
key, valueStr, err := parseKeyValue(override)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldType, err := getFieldType(defaults, key)
|
|
||||||
if err != nil {
|
|
||||||
return fmtErrorf("unknown config key: %s", key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the value based on the field type
|
|
||||||
var parsedValue any
|
|
||||||
switch fieldType {
|
|
||||||
case "int64":
|
|
||||||
parsedValue, err = strconv.ParseInt(valueStr, 10, 64)
|
|
||||||
case "string":
|
|
||||||
parsedValue = valueStr
|
|
||||||
case "bool":
|
|
||||||
parsedValue, err = strconv.ParseBool(valueStr)
|
|
||||||
case "float64":
|
|
||||||
parsedValue, err = strconv.ParseFloat(valueStr, 64)
|
|
||||||
default:
|
|
||||||
return fmtErrorf("unsupported type for key '%s': %s", key, fieldType)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmtErrorf("invalid value format for '%s': %w", key, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
overrideMap[strings.ToLower(key)] = parsedValue
|
|
||||||
}
|
|
||||||
|
|
||||||
return l.Init(overrideMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getFieldType uses reflection to determine the type of a config field
|
|
||||||
func getFieldType(cfg *Config, fieldName string) (string, error) {
|
|
||||||
v := reflect.ValueOf(cfg).Elem()
|
|
||||||
t := v.Type()
|
|
||||||
|
|
||||||
fieldName = strings.ToLower(fieldName)
|
|
||||||
|
|
||||||
for i := 0; i < t.NumField(); i++ {
|
|
||||||
field := t.Field(i)
|
|
||||||
tomlTag := field.Tag.Get("toml")
|
|
||||||
if strings.ToLower(tomlTag) == fieldName {
|
|
||||||
switch field.Type.Kind() {
|
|
||||||
case reflect.String:
|
|
||||||
return "string", nil
|
|
||||||
case reflect.Int64:
|
|
||||||
return "int64", nil
|
|
||||||
case reflect.Float64:
|
|
||||||
return "float64", nil
|
|
||||||
case reflect.Bool:
|
|
||||||
return "bool", nil
|
|
||||||
default:
|
|
||||||
return "", fmt.Errorf("unsupported field type: %v", field.Type.Kind())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", fmt.Errorf("field not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shutdown gracefully closes the logger, attempting to flush pending records
|
// Shutdown gracefully closes the logger, attempting to flush pending records
|
||||||
// If no timeout is provided, uses a default of 2x flush interval
|
// If no timeout is provided, uses a default of 2x flush interval
|
||||||
func (l *Logger) Shutdown(timeout ...time.Duration) error {
|
func (l *Logger) Shutdown(timeout ...time.Duration) error {
|
||||||
|
|||||||
@ -153,7 +153,7 @@ func (l *Logger) getDiskFreeSpace(path string) (int64, error) {
|
|||||||
if err := syscall.Statfs(path, &stat); err != nil {
|
if err := syscall.Statfs(path, &stat); err != nil {
|
||||||
return 0, fmtErrorf("failed to get disk stats for '%s': %w", path, err)
|
return 0, fmtErrorf("failed to get disk stats for '%s': %w", path, err)
|
||||||
}
|
}
|
||||||
availableBytes := int64(stat.Bavail) * int64(stat.Bsize)
|
availableBytes := int64(stat.Bavail) * stat.Bsize
|
||||||
return availableBytes, nil
|
return availableBytes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
131
utility.go
131
utility.go
@ -112,135 +112,4 @@ func Level(levelStr string) (int64, error) {
|
|||||||
default:
|
default:
|
||||||
return 0, fmtErrorf("invalid level string: '%s' (use debug, info, warn, error, proc, disk, sys)", levelStr)
|
return 0, fmtErrorf("invalid level string: '%s' (use debug, info, warn, error, proc, disk, sys)", levelStr)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// validateConfigValue validates a single configuration field
|
|
||||||
func validateConfigValue(key string, value any) error {
|
|
||||||
keyLower := strings.ToLower(key)
|
|
||||||
switch keyLower {
|
|
||||||
case "name":
|
|
||||||
v, ok := value.(string)
|
|
||||||
if !ok {
|
|
||||||
return fmtErrorf("name must be string, got %T", value)
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(v) == "" {
|
|
||||||
return fmtErrorf("log name cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
case "format":
|
|
||||||
v, ok := value.(string)
|
|
||||||
if !ok {
|
|
||||||
return fmtErrorf("format must be string, got %T", value)
|
|
||||||
}
|
|
||||||
if v != "txt" && v != "json" && v != "raw" {
|
|
||||||
return fmtErrorf("invalid format: '%s' (use txt, json, or raw)", v)
|
|
||||||
}
|
|
||||||
|
|
||||||
case "extension":
|
|
||||||
v, ok := value.(string)
|
|
||||||
if !ok {
|
|
||||||
return fmtErrorf("extension must be string, got %T", value)
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(v, ".") {
|
|
||||||
return fmtErrorf("extension should not start with dot: %s", v)
|
|
||||||
}
|
|
||||||
|
|
||||||
case "timestamp_format":
|
|
||||||
v, ok := value.(string)
|
|
||||||
if !ok {
|
|
||||||
return fmtErrorf("timestamp_format must be string, got %T", value)
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(v) == "" {
|
|
||||||
return fmtErrorf("timestamp_format cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
case "buffer_size":
|
|
||||||
v, ok := value.(int64)
|
|
||||||
if !ok {
|
|
||||||
return fmtErrorf("buffer_size must be int64, got %T", value)
|
|
||||||
}
|
|
||||||
if v <= 0 {
|
|
||||||
return fmtErrorf("buffer_size must be positive: %d", v)
|
|
||||||
}
|
|
||||||
|
|
||||||
case "max_size_mb", "max_total_size_mb", "min_disk_free_mb":
|
|
||||||
v, ok := value.(int64)
|
|
||||||
if !ok {
|
|
||||||
return fmtErrorf("%s must be int64, got %T", key, value)
|
|
||||||
}
|
|
||||||
if v < 0 {
|
|
||||||
return fmtErrorf("%s cannot be negative: %d", key, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
case "flush_interval_ms", "disk_check_interval_ms", "min_check_interval_ms", "max_check_interval_ms":
|
|
||||||
v, ok := value.(int64)
|
|
||||||
if !ok {
|
|
||||||
return fmtErrorf("%s must be int64, got %T", key, value)
|
|
||||||
}
|
|
||||||
if v <= 0 {
|
|
||||||
return fmtErrorf("%s must be positive milliseconds: %d", key, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
case "trace_depth":
|
|
||||||
v, ok := value.(int64)
|
|
||||||
if !ok {
|
|
||||||
return fmtErrorf("trace_depth must be int64, got %T", value)
|
|
||||||
}
|
|
||||||
if v < 0 || v > 10 {
|
|
||||||
return fmtErrorf("trace_depth must be between 0 and 10: %d", v)
|
|
||||||
}
|
|
||||||
|
|
||||||
case "retention_period_hrs", "retention_check_mins":
|
|
||||||
v, ok := value.(float64)
|
|
||||||
if !ok {
|
|
||||||
return fmtErrorf("%s must be float64, got %T", key, value)
|
|
||||||
}
|
|
||||||
if v < 0 {
|
|
||||||
return fmtErrorf("%s cannot be negative: %f", key, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
case "heartbeat_level":
|
|
||||||
v, ok := value.(int64)
|
|
||||||
if !ok {
|
|
||||||
return fmtErrorf("heartbeat_level must be int64, got %T", value)
|
|
||||||
}
|
|
||||||
if v < 0 || v > 3 {
|
|
||||||
return fmtErrorf("heartbeat_level must be between 0 and 3: %d", v)
|
|
||||||
}
|
|
||||||
|
|
||||||
case "heartbeat_interval_s":
|
|
||||||
_, ok := value.(int64)
|
|
||||||
if !ok {
|
|
||||||
return fmtErrorf("heartbeat_interval_s must be int64, got %T", value)
|
|
||||||
}
|
|
||||||
// Note: only validate positive if heartbeat is enabled (cross-field validation)
|
|
||||||
|
|
||||||
case "stdout_target":
|
|
||||||
v, ok := value.(string)
|
|
||||||
if !ok {
|
|
||||||
return fmtErrorf("stdout_target must be string, got %T", value)
|
|
||||||
}
|
|
||||||
if v != "stdout" && v != "stderr" {
|
|
||||||
return fmtErrorf("invalid stdout_target: '%s' (use stdout or stderr)", v)
|
|
||||||
}
|
|
||||||
|
|
||||||
case "level":
|
|
||||||
// Level validation if needed
|
|
||||||
_, ok := value.(int64)
|
|
||||||
if !ok {
|
|
||||||
return fmtErrorf("level must be int64, got %T", value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fields that don't need validation beyond type
|
|
||||||
case "directory", "show_timestamp", "show_level", "enable_adaptive_interval",
|
|
||||||
"enable_periodic_sync", "enable_stdout", "disable_file", "internal_errors_to_stderr":
|
|
||||||
// Type checking handled by config system
|
|
||||||
return nil
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Unknown field - let config system handle it
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user