e1.8.0 Raw (unformatted) logging support added.
This commit is contained in:
82
config.go
82
config.go
@ -2,7 +2,6 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -107,59 +106,44 @@ func DefaultConfig() *Config {
|
||||
|
||||
// validate performs basic sanity checks on the configuration values.
|
||||
func (c *Config) validate() error {
|
||||
if strings.TrimSpace(c.Name) == "" {
|
||||
return fmtErrorf("log name cannot be empty")
|
||||
// Individual field validations
|
||||
fields := map[string]any{
|
||||
"name": c.Name,
|
||||
"format": c.Format,
|
||||
"extension": c.Extension,
|
||||
"timestamp_format": c.TimestampFormat,
|
||||
"buffer_size": c.BufferSize,
|
||||
"max_size_mb": c.MaxSizeMB,
|
||||
"max_total_size_mb": c.MaxTotalSizeMB,
|
||||
"min_disk_free_mb": c.MinDiskFreeMB,
|
||||
"flush_interval_ms": c.FlushIntervalMs,
|
||||
"disk_check_interval_ms": c.DiskCheckIntervalMs,
|
||||
"min_check_interval_ms": c.MinCheckIntervalMs,
|
||||
"max_check_interval_ms": c.MaxCheckIntervalMs,
|
||||
"trace_depth": c.TraceDepth,
|
||||
"retention_period_hrs": c.RetentionPeriodHrs,
|
||||
"retention_check_mins": c.RetentionCheckMins,
|
||||
"heartbeat_level": c.HeartbeatLevel,
|
||||
"heartbeat_interval_s": c.HeartbeatIntervalS,
|
||||
"stdout_target": c.StdoutTarget,
|
||||
"level": c.Level,
|
||||
}
|
||||
if c.Format != "txt" && c.Format != "json" {
|
||||
return fmtErrorf("invalid format: '%s' (use txt or json)", c.Format)
|
||||
}
|
||||
if strings.TrimSpace(c.TimestampFormat) == "" {
|
||||
return fmtErrorf("timestamp_format cannot be empty")
|
||||
}
|
||||
if c.BufferSize <= 0 {
|
||||
return fmtErrorf("buffer_size must be positive: %d", c.BufferSize)
|
||||
}
|
||||
if c.MaxSizeMB < 0 {
|
||||
return fmtErrorf("max_size_mb cannot be negative: %d", c.MaxSizeMB)
|
||||
}
|
||||
if c.MaxTotalSizeMB < 0 {
|
||||
return fmtErrorf("max_total_size_mb cannot be negative: %d", c.MaxTotalSizeMB)
|
||||
}
|
||||
if c.MinDiskFreeMB < 0 {
|
||||
return fmtErrorf("min_disk_free_mb cannot be negative: %d", c.MinDiskFreeMB)
|
||||
}
|
||||
if c.FlushIntervalMs <= 0 {
|
||||
return fmtErrorf("flush_interval_ms must be positive milliseconds: %d", c.FlushIntervalMs)
|
||||
}
|
||||
if c.DiskCheckIntervalMs <= 0 {
|
||||
return fmtErrorf("disk_check_interval_ms must be positive milliseconds: %d", c.DiskCheckIntervalMs)
|
||||
}
|
||||
if c.MinCheckIntervalMs <= 0 {
|
||||
return fmtErrorf("min_check_interval_ms must be positive milliseconds: %d", c.MinCheckIntervalMs)
|
||||
}
|
||||
if c.MaxCheckIntervalMs <= 0 {
|
||||
return fmtErrorf("max_check_interval_ms must be positive milliseconds: %d", c.MaxCheckIntervalMs)
|
||||
|
||||
for key, value := range fields {
|
||||
if err := validateConfigValue(key, value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Cross-field validations
|
||||
if c.MinCheckIntervalMs > c.MaxCheckIntervalMs {
|
||||
return fmtErrorf("min_check_interval_ms (%d) cannot be greater than max_check_interval_ms (%d)", c.MinCheckIntervalMs, c.MaxCheckIntervalMs)
|
||||
}
|
||||
if c.TraceDepth < 0 || c.TraceDepth > 10 {
|
||||
return fmtErrorf("trace_depth must be between 0 and 10: %d", c.TraceDepth)
|
||||
}
|
||||
if c.RetentionPeriodHrs < 0 {
|
||||
return fmtErrorf("retention_period_hrs cannot be negative: %f", c.RetentionPeriodHrs)
|
||||
}
|
||||
if c.RetentionCheckMins < 0 {
|
||||
return fmtErrorf("retention_check_mins cannot be negative: %f", c.RetentionCheckMins)
|
||||
}
|
||||
if c.HeartbeatLevel < 0 || c.HeartbeatLevel > 3 {
|
||||
return fmtErrorf("heartbeat_level must be between 0 and 3: %d", c.HeartbeatLevel)
|
||||
return fmtErrorf("min_check_interval_ms (%d) cannot be greater than max_check_interval_ms (%d)",
|
||||
c.MinCheckIntervalMs, c.MaxCheckIntervalMs)
|
||||
}
|
||||
|
||||
if c.HeartbeatLevel > 0 && c.HeartbeatIntervalS <= 0 {
|
||||
return fmtErrorf("heartbeat_interval_s must be positive when heartbeat is enabled: %d", c.HeartbeatIntervalS)
|
||||
}
|
||||
if c.StdoutTarget != "stdout" && c.StdoutTarget != "stderr" {
|
||||
return fmtErrorf("invalid stdout_target: '%s' (use stdout or stderr)", c.StdoutTarget)
|
||||
return fmtErrorf("heartbeat_interval_s must be positive when heartbeat is enabled: %d",
|
||||
c.HeartbeatIntervalS)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
72
example/raw/main.go
Normal file
72
example/raw/main.go
Normal file
@ -0,0 +1,72 @@
|
||||
// FILE: example/raw/main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/lixenwraith/log"
|
||||
)
|
||||
|
||||
// TestPayload defines a struct for testing complex type serialization.
|
||||
type TestPayload struct {
|
||||
RequestID uint64
|
||||
User string
|
||||
Metrics map[string]float64
|
||||
}
|
||||
|
||||
func main() {
|
||||
fmt.Println("--- Logger Raw Format Test ---")
|
||||
|
||||
// --- 1. Define the records to be tested ---
|
||||
// Record 1: A byte slice with special characters (newline, tab, null).
|
||||
byteRecord := []byte("binary\ndata\twith\x00null")
|
||||
|
||||
// Record 2: A struct containing a uint64, a string, and a map.
|
||||
structRecord := TestPayload{
|
||||
RequestID: 9223372036854775807, // A large uint64
|
||||
User: "test_user",
|
||||
Metrics: map[string]float64{
|
||||
"latency_ms": 15.7,
|
||||
"cpu_percent": 88.2,
|
||||
},
|
||||
}
|
||||
|
||||
// --- 2. Test on-demand raw logging using Logger.Write() ---
|
||||
// This method produces raw output regardless of the global format setting.
|
||||
fmt.Println("\n[1] Testing on-demand raw output via Logger.Write()")
|
||||
logger1 := log.NewLogger()
|
||||
// Use default config, but enable stdout and disable file output for this test.
|
||||
err := logger1.InitWithDefaults("enable_stdout=true", "disable_file=false")
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to initialize logger: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
logger1.Write("Byte Record ->", byteRecord)
|
||||
logger1.Write("Struct Record ->", structRecord)
|
||||
// Wait briefly for the async processor to handle the logs.
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
logger1.Shutdown()
|
||||
|
||||
// --- 3. Test instance-wide raw logging using format="raw" ---
|
||||
// Here, standard methods like Info() will produce raw output.
|
||||
fmt.Println("\n[2] Testing instance-wide raw output via format=\"raw\"")
|
||||
logger2 := log.NewLogger()
|
||||
err = logger2.InitWithDefaults(
|
||||
"enable_stdout=true",
|
||||
"disable_file=false",
|
||||
"format=raw",
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to initialize logger: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
logger2.Info("Byte Record ->", byteRecord)
|
||||
logger2.Info("Struct Record ->", structRecord)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
logger2.Shutdown()
|
||||
|
||||
fmt.Println("\n--- Test Complete ---")
|
||||
}
|
||||
167
format.go
167
format.go
@ -2,10 +2,15 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
)
|
||||
|
||||
// serializer manages the buffered writing of log entries.
|
||||
@ -27,16 +32,176 @@ func (s *serializer) reset() {
|
||||
s.buf = s.buf[:0]
|
||||
}
|
||||
|
||||
// serialize converts log entries to the configured format, JSON or (default) text.
|
||||
// serialize converts log entries to the configured format, JSON, raw, or (default) text.
|
||||
func (s *serializer) serialize(format string, flags int64, timestamp time.Time, level int64, trace string, args []any) []byte {
|
||||
s.reset()
|
||||
|
||||
// 1. Prioritize the on-demand flag from Write()
|
||||
if flags&FlagRaw != 0 {
|
||||
return s.serializeRaw(args)
|
||||
}
|
||||
|
||||
// 2. Handle the instance-wide configuration setting
|
||||
if format == "raw" {
|
||||
return s.serializeRaw(args)
|
||||
}
|
||||
|
||||
if format == "json" {
|
||||
return s.serializeJSON(flags, timestamp, level, trace, args)
|
||||
}
|
||||
return s.serializeText(flags, timestamp, level, trace, args)
|
||||
}
|
||||
|
||||
// serializeRaw formats args as space-separated strings without metadata or newline.
|
||||
// This is used for both format="raw" configuration and Logger.Write() calls.
|
||||
func (s *serializer) serializeRaw(args []any) []byte {
|
||||
needsSpace := false
|
||||
|
||||
for _, arg := range args {
|
||||
if needsSpace {
|
||||
s.buf = append(s.buf, ' ')
|
||||
}
|
||||
s.writeRawValue(arg)
|
||||
needsSpace = true
|
||||
}
|
||||
|
||||
// No newline appended for raw format
|
||||
return s.buf
|
||||
}
|
||||
|
||||
// writeRawValue converts any value to its raw string representation.
|
||||
// fallback to go-spew/spew with data structure information for types that are not explicitly supported.
|
||||
func (s *serializer) writeRawValue(v any) {
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
s.buf = append(s.buf, val...)
|
||||
case int:
|
||||
s.buf = strconv.AppendInt(s.buf, int64(val), 10)
|
||||
case int64:
|
||||
s.buf = strconv.AppendInt(s.buf, val, 10)
|
||||
case uint:
|
||||
s.buf = strconv.AppendUint(s.buf, uint64(val), 10)
|
||||
case uint64:
|
||||
s.buf = strconv.AppendUint(s.buf, val, 10)
|
||||
case float32:
|
||||
s.buf = strconv.AppendFloat(s.buf, float64(val), 'f', -1, 32)
|
||||
case float64:
|
||||
s.buf = strconv.AppendFloat(s.buf, val, 'f', -1, 64)
|
||||
case bool:
|
||||
s.buf = strconv.AppendBool(s.buf, val)
|
||||
case nil:
|
||||
s.buf = append(s.buf, "nil"...)
|
||||
case time.Time:
|
||||
s.buf = val.AppendFormat(s.buf, s.timestampFormat)
|
||||
case error:
|
||||
s.buf = append(s.buf, val.Error()...)
|
||||
case fmt.Stringer:
|
||||
s.buf = append(s.buf, val.String()...)
|
||||
case []byte:
|
||||
s.buf = hex.AppendEncode(s.buf, val) // prevent special character corruption
|
||||
default:
|
||||
// For all other types (structs, maps, pointers, arrays, etc.), delegate to spew.
|
||||
// It is not the intended use of raw logging.
|
||||
// The output of such cases are structured and have type and size information set by spew.
|
||||
// Converting to string similar to non-raw logs is not used to avoid binary log corruption.
|
||||
var b bytes.Buffer
|
||||
|
||||
// Use a custom dumper for log-friendly, compact output.
|
||||
dumper := &spew.ConfigState{
|
||||
Indent: " ",
|
||||
MaxDepth: 10,
|
||||
DisablePointerAddresses: true, // Cleaner for logs
|
||||
DisableCapacities: true, // Less noise
|
||||
SortKeys: true, // Consistent map output
|
||||
}
|
||||
|
||||
dumper.Fdump(&b, val)
|
||||
|
||||
// Trim trailing new line added by spew
|
||||
s.buf = append(s.buf, bytes.TrimSpace(b.Bytes())...)
|
||||
}
|
||||
}
|
||||
|
||||
// This is the safe, dependency-free replacement for fmt.Sprintf.
|
||||
func (s *serializer) reflectValue(v reflect.Value) {
|
||||
// Safely handle invalid, nil pointer, or nil interface values.
|
||||
if !v.IsValid() {
|
||||
s.buf = append(s.buf, "nil"...)
|
||||
return
|
||||
}
|
||||
// Dereference pointers and interfaces to get the concrete value.
|
||||
// Recurse to handle multiple levels of pointers.
|
||||
kind := v.Kind()
|
||||
if kind == reflect.Ptr || kind == reflect.Interface {
|
||||
if v.IsNil() {
|
||||
s.buf = append(s.buf, "nil"...)
|
||||
return
|
||||
}
|
||||
s.reflectValue(v.Elem())
|
||||
return
|
||||
}
|
||||
|
||||
switch kind {
|
||||
case reflect.String:
|
||||
s.buf = append(s.buf, v.String()...)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
s.buf = strconv.AppendInt(s.buf, v.Int(), 10)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
s.buf = strconv.AppendUint(s.buf, v.Uint(), 10)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
s.buf = strconv.AppendFloat(s.buf, v.Float(), 'f', -1, 64)
|
||||
case reflect.Bool:
|
||||
s.buf = strconv.AppendBool(s.buf, v.Bool())
|
||||
|
||||
case reflect.Slice, reflect.Array:
|
||||
// Check if it's a byte slice ([]uint8) and hex-encode it for safety.
|
||||
if v.Type().Elem().Kind() == reflect.Uint8 {
|
||||
s.buf = append(s.buf, "0x"...)
|
||||
s.buf = hex.AppendEncode(s.buf, v.Bytes())
|
||||
return
|
||||
}
|
||||
s.buf = append(s.buf, '[')
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
if i > 0 {
|
||||
s.buf = append(s.buf, ' ')
|
||||
}
|
||||
s.reflectValue(v.Index(i))
|
||||
}
|
||||
s.buf = append(s.buf, ']')
|
||||
|
||||
case reflect.Struct:
|
||||
s.buf = append(s.buf, '{')
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
if !v.Type().Field(i).IsExported() {
|
||||
continue // Skip unexported fields
|
||||
}
|
||||
if i > 0 {
|
||||
s.buf = append(s.buf, ' ')
|
||||
}
|
||||
s.buf = append(s.buf, v.Type().Field(i).Name...)
|
||||
s.buf = append(s.buf, ':')
|
||||
s.reflectValue(v.Field(i))
|
||||
}
|
||||
s.buf = append(s.buf, '}')
|
||||
|
||||
case reflect.Map:
|
||||
s.buf = append(s.buf, '{')
|
||||
for i, key := range v.MapKeys() {
|
||||
if i > 0 {
|
||||
s.buf = append(s.buf, ' ')
|
||||
}
|
||||
s.reflectValue(key)
|
||||
s.buf = append(s.buf, ':')
|
||||
s.reflectValue(v.MapIndex(key))
|
||||
}
|
||||
s.buf = append(s.buf, '}')
|
||||
|
||||
default:
|
||||
// As a final fallback, use fmt, but this should rarely be hit.
|
||||
s.buf = append(s.buf, fmt.Sprint(v.Interface())...)
|
||||
}
|
||||
}
|
||||
|
||||
// serializeJSON formats log entries as JSON (time, level, trace, fields).
|
||||
func (s *serializer) serializeJSON(flags int64, timestamp time.Time, level int64, trace string, args []any) []byte {
|
||||
s.buf = append(s.buf, '{')
|
||||
|
||||
1
go.mod
1
go.mod
@ -3,6 +3,7 @@ module github.com/lixenwraith/log
|
||||
go 1.24.5
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/lixenwraith/config v0.0.0-20250712170030-7d38402e0497
|
||||
github.com/panjf2000/gnet/v2 v2.9.1
|
||||
github.com/valyala/fasthttp v1.63.0
|
||||
|
||||
2
go.sum
2
go.sum
@ -6,8 +6,6 @@ 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/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/lixenwraith/config v0.0.0-20250701170607-8515fa0543b6 h1:qE4SpAJWFaLkdRyE0FjTPBBRYE7LOvcmRCB5p86W73Q=
|
||||
github.com/lixenwraith/config v0.0.0-20250701170607-8515fa0543b6/go.mod h1:4wPJ3HnLrYrtUwTinngCsBgtdIXsnxkLa7q4KAIbwY8=
|
||||
github.com/lixenwraith/config v0.0.0-20250712170030-7d38402e0497 h1:ixTIdJSd945n/IhMRwGwQVmQnQ1nUr5z1wn31jXq9FU=
|
||||
github.com/lixenwraith/config v0.0.0-20250712170030-7d38402e0497/go.mod h1:y7kgDrWIFROWJJ6ASM/SPTRRAj27FjRGWh2SDLcdQ68=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
|
||||
12
interface.go
12
interface.go
@ -22,8 +22,9 @@ const (
|
||||
|
||||
// Record flags for controlling output structure
|
||||
const (
|
||||
FlagShowTimestamp int64 = 0b01
|
||||
FlagShowLevel int64 = 0b10
|
||||
FlagShowTimestamp int64 = 0b001
|
||||
FlagShowLevel int64 = 0b010
|
||||
FlagRaw int64 = 0b100
|
||||
FlagDefault = FlagShowTimestamp | FlagShowLevel
|
||||
)
|
||||
|
||||
@ -104,4 +105,11 @@ func (l *Logger) Message(args ...any) {
|
||||
// LogTrace writes a timestamp record with call trace but no level info.
|
||||
func (l *Logger) LogTrace(depth int, args ...any) {
|
||||
l.log(FlagShowTimestamp, LevelInfo, int64(depth), args...)
|
||||
}
|
||||
|
||||
// Write outputs raw, unformatted data regardless of configured format.
|
||||
// This method bypasses all formatting (timestamps, levels, JSON structure)
|
||||
// and writes args as space-separated strings without a trailing newline.
|
||||
func (l *Logger) Write(args ...any) {
|
||||
l.log(FlagRaw, LevelInfo, 0, args...)
|
||||
}
|
||||
176
utility.go
176
utility.go
@ -92,51 +92,6 @@ func parseKeyValue(arg string) (string, string, error) {
|
||||
return key, value, nil
|
||||
}
|
||||
|
||||
// validateConfigValue checks ranges and specific constraints for parsed config values.
|
||||
func validateConfigValue(key string, value interface{}) error {
|
||||
keyLower := strings.ToLower(key)
|
||||
|
||||
switch keyLower {
|
||||
case "name":
|
||||
if v, ok := value.(string); ok && strings.TrimSpace(v) == "" {
|
||||
return fmtErrorf("log name cannot be empty")
|
||||
}
|
||||
case "format":
|
||||
if v, ok := value.(string); ok && v != "txt" && v != "json" {
|
||||
return fmtErrorf("invalid format: '%s' (use txt or json)", v)
|
||||
}
|
||||
case "extension":
|
||||
if v, ok := value.(string); ok && strings.HasPrefix(v, ".") {
|
||||
return fmtErrorf("extension should not start with dot: %s", v)
|
||||
}
|
||||
case "timestamp_format":
|
||||
if v, ok := value.(string); ok && strings.TrimSpace(v) == "" {
|
||||
return fmtErrorf("timestamp_format cannot be empty")
|
||||
}
|
||||
case "buffer_size":
|
||||
if v, ok := value.(int64); ok && v <= 0 {
|
||||
return fmtErrorf("buffer_size must be positive: %d", v)
|
||||
}
|
||||
case "max_size_mb", "max_total_size_mb", "min_disk_free_mb":
|
||||
if v, ok := value.(int64); ok && v < 0 {
|
||||
return fmtErrorf("%s cannot be negative: %d", key, v)
|
||||
}
|
||||
case "flush_timer", "disk_check_interval_ms", "min_check_interval_ms", "max_check_interval_ms":
|
||||
if v, ok := value.(int64); ok && v <= 0 {
|
||||
return fmtErrorf("%s must be positive milliseconds: %d", key, v)
|
||||
}
|
||||
case "trace_depth":
|
||||
if v, ok := value.(int64); ok && (v < 0 || v > 10) {
|
||||
return fmtErrorf("trace_depth must be between 0 and 10: %d", v)
|
||||
}
|
||||
case "retention_period", "retention_check_interval":
|
||||
if v, ok := value.(float64); ok && v < 0 {
|
||||
return fmtErrorf("%s cannot be negative: %f", key, v)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Level converts level string to numeric constant.
|
||||
func Level(levelStr string) (int64, error) {
|
||||
switch strings.ToLower(strings.TrimSpace(levelStr)) {
|
||||
@ -157,4 +112,135 @@ func Level(levelStr string) (int64, error) {
|
||||
default:
|
||||
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