-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathconfig.go
289 lines (248 loc) · 10.1 KB
/
config.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
package logger
import (
"context"
"fmt"
"os"
"strings"
"sync"
"sync/atomic"
"time"
)
// Package level variables maintaining logger state and configuration.
// Thread-safety is ensured through atomic operations and mutex locks.
var (
isInitialized atomic.Bool
initMu sync.Mutex
loggerDisabled atomic.Bool
logLevel atomic.Value // stores int64
mu sync.RWMutex
)
// LoggerConfig defines the logger configuration parameters.
// All fields can be configured via JSON or TOML configuration files.
type LoggerConfig struct {
Level int64 `json:"level" toml:"level"` // LevelDebug, LevelInfo, LevelWarn, LevelError
Name string `json:"name" toml:"name"` // Base name for log files
Directory string `json:"directory" toml:"directory"` // Directory to store log files
Format string `json:"format" toml:"format"` // Serialized output file type: txt, json
Extension string `json:"extension" toml:"extension"` // Log file extension (default "log", empty = use format)
ShowTimestamp bool `json:"show_timestamp" toml:"show_timestamp"` // Enable time stamp (default enabled)
ShowLevel bool `json:"show_level" toml:"show_level"` // Enable level (default enabled)
BufferSize int64 `json:"buffer_size" toml:"buffer_size"` // Channel buffer size
MaxSizeMB int64 `json:"max_size_mb" toml:"max_size_mb"` // Max size of each log file in MB
MaxTotalSizeMB int64 `json:"max_total_size_mb" toml:"max_total_size_mb"` // Max total size of the log folder in MB to trigger old log deletion/pause logging
MinDiskFreeMB int64 `json:"min_disk_free_mb" toml:"min_disk_free_mb"` // Min available free space in MB to trigger old log deletion/pause logging
FlushTimer int64 `json:"flush_timer" toml:"flush_timer"` // Periodically forces writing logs to the disk to avoid missing logs on program shutdown
TraceDepth int64 `json:"trace_depth" toml:"trace_depth"` // 0-10, 0 disables tracing
RetentionPeriod float64 `json:"retention_period" toml:"retention_period"` // RetentionPeriod defines how long to keep log files in hours. Zero disables retention.
RetentionCheckInterval float64 `json:"retention_check_interval" toml:"retention_check_interval"` // RetentionCheckInterval defines how often to check for expired logs in minutes if retention is enabled.
}
// configLogger initializes the logger with the provided configuration.
// It validates the configuration and sets up the logging infrastructure including file management and buffering.
func configLogger(ctx context.Context, cfg ...*LoggerConfig) error {
// defaultConfig values are used if value is not provided by the user
defaultConfig := &LoggerConfig{
Level: LevelInfo,
Name: "log",
Directory: "./logs",
Format: "txt",
Extension: "log",
ShowTimestamp: true,
ShowLevel: true,
BufferSize: 1024,
MaxSizeMB: 10,
MaxTotalSizeMB: 50,
MinDiskFreeMB: 100,
FlushTimer: 100,
TraceDepth: 0,
RetentionPeriod: 0.0,
RetentionCheckInterval: 60.0,
}
if len(cfg) == 0 {
return initLogger(ctx, defaultConfig)
}
userConfig := cfg[0]
var mergedCfg *LoggerConfig
if isInitialized.Load() {
// Merge with current running config
currentCfg := &LoggerConfig{
Level: logLevel.Load().(int64),
Name: name,
Directory: directory,
Format: format,
Extension: extension,
ShowTimestamp: flags&FlagShowTimestamp != 0,
ShowLevel: flags&FlagShowLevel != 0,
BufferSize: bufferSize.Load(),
MaxSizeMB: maxSizeMB,
MaxTotalSizeMB: maxTotalSizeMB,
MinDiskFreeMB: minDiskFreeMB,
FlushTimer: int64(flushTimer / time.Millisecond),
TraceDepth: traceDepth,
RetentionPeriod: float64(retentionPeriod / time.Hour),
RetentionCheckInterval: float64(retentionCheck / time.Minute),
}
mergedCfg = mergeConfigs(currentCfg, userConfig)
} else {
mergedCfg = mergeConfigs(defaultConfig, userConfig)
}
return initLogger(ctx, mergedCfg)
}
// mergeConfigs overrides base values for non-zero values in override
func mergeConfigs(base, override *LoggerConfig) *LoggerConfig {
return &LoggerConfig{
Level: getConfigValue(base.Level, override.Level),
Name: getConfigValue(base.Name, override.Name),
Directory: getConfigValue(base.Directory, override.Directory),
Format: getConfigValue(base.Format, override.Format),
Extension: getConfigValue(base.Extension, override.Extension),
ShowTimestamp: getConfigValue(base.ShowTimestamp, override.ShowTimestamp),
ShowLevel: getConfigValue(base.ShowLevel, override.ShowLevel),
BufferSize: getConfigValue(base.BufferSize, override.BufferSize),
MaxSizeMB: getConfigValue(base.MaxSizeMB, override.MaxSizeMB),
MaxTotalSizeMB: getConfigValue(base.MaxTotalSizeMB, override.MaxTotalSizeMB),
MinDiskFreeMB: getConfigValue(base.MinDiskFreeMB, override.MinDiskFreeMB),
FlushTimer: getConfigValue(base.FlushTimer, override.FlushTimer),
TraceDepth: getConfigValue(base.TraceDepth, override.TraceDepth),
RetentionPeriod: getConfigValue(base.RetentionPeriod, override.RetentionPeriod),
RetentionCheckInterval: getConfigValue(base.RetentionCheckInterval, override.RetentionCheckInterval),
}
}
// initLogger configures and starts the logging infrastructure with the provided configuration.
// It handles initialization of files, channels, and background processing while ensuring thread safety.
func initLogger(ctx context.Context, cfg *LoggerConfig) error {
mu.Lock()
defer mu.Unlock()
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := applyConfig(ctx, cfg); err != nil {
return err
}
if err := os.MkdirAll(directory, 0755); err != nil {
return fmt.Errorf("failed to create log directory: %w", err)
}
// Handle reconfiguration
if isInitialized.Load() {
if processCancel != nil {
processCancel()
}
if bufferSize.Load() != cfg.BufferSize {
close(logChannel)
}
}
// Initialize new log file and logger instance
logFile, err := createNewLogFile(ctx)
if err != nil {
return fmt.Errorf("failed to create initial log file: %w", err)
}
currentFile.Store(logFile)
logChannel = make(chan logRecord, bufferSize.Load())
processCtx, processCancel = context.WithCancel(ctx)
go processLogs()
isInitialized.Store(true)
return nil
}
}
// applyConfig sets the running config
func applyConfig(ctx context.Context, cfg *LoggerConfig) error {
flags = 0
if cfg.ShowLevel {
flags |= FlagShowLevel
}
if cfg.ShowTimestamp {
flags |= FlagShowTimestamp
}
directory = cfg.Directory
if directory == "" {
directory = "."
}
name = cfg.Name
format = cfg.Format
if cfg.Extension != "" {
if strings.HasPrefix(cfg.Extension, ".") {
return fmt.Errorf("extension should not start with dot: %s", cfg.Extension)
}
extension = cfg.Extension
} else if cfg.Format != "" {
// Use format as extension if no explicit extension provided
extension = cfg.Format
} else {
extension = "log"
}
maxSizeMB = cfg.MaxSizeMB
maxTotalSizeMB = cfg.MaxTotalSizeMB
minDiskFreeMB = cfg.MinDiskFreeMB
flushTimer = time.Duration(cfg.FlushTimer) * time.Millisecond
retentionPeriod = time.Duration(cfg.RetentionPeriod * float64(time.Hour))
retentionCheck = time.Duration(cfg.RetentionCheckInterval * float64(time.Minute))
newBufferSize := cfg.BufferSize
if newBufferSize < 1 {
newBufferSize = 1000
}
if maxTotalSizeMB < 0 || minDiskFreeMB < 0 {
return fmt.Errorf("invalid disk space configuration")
}
if cfg.TraceDepth < 0 || cfg.TraceDepth > 10 {
return fmt.Errorf("invalid trace depth: must be between 0 and 10")
}
traceDepth = cfg.TraceDepth
logLevel.Store(cfg.Level)
bufferSize.Store(newBufferSize)
return nil
}
// getConfigValue returns defaultVal if cfgVal equals the zero value for type T,
// otherwise returns cfgVal. Type T must satisfy the comparable constraint.
// This is commonly used for merging configuration values with their defaults.
func getConfigValue[T comparable](defaultVal, cfgVal T) T {
var zero T
if cfgVal == zero {
return defaultVal
}
return cfgVal
}
// shutdownOnce ensures the logger shutdown routine executes exactly once,
// even if multiple shutdown paths are triggered simultaneously.
var shutdownOnce sync.Once
// shutdownLogger performs a graceful shutdown of the logger, ensuring all buffered logs
// are written and files are properly closed. It respects context cancellation for timeout control.
func shutdownLogger(ctx context.Context) error {
mu.Lock()
defer mu.Unlock()
if !isInitialized.Load() {
return nil
}
timer := time.NewTimer(2 * flushTimer)
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
}
loggerDisabled.Store(true)
isInitialized.Store(false)
if processCancel != nil {
processCancel()
}
close(logChannel)
// Final file operations
if currentFile := currentFile.Load().(*os.File); currentFile != nil {
syncDone := make(chan error, 1)
go func() {
syncDone <- currentFile.Sync()
}()
// Wait for sync or context cancellation
select {
case err := <-syncDone:
if err != nil {
return fmt.Errorf("failed to sync log file: %w", err)
}
case <-ctx.Done():
return ctx.Err()
}
// Close file - this should be quick and not block
if err := currentFile.Close(); err != nil {
return fmt.Errorf("failed to close log file: %w", err)
}
}
return nil
}