1717package log
1818
1919import (
20+ "fmt"
2021 "io"
2122 "os"
2223 "path/filepath"
24+ "strings"
25+ "sync"
26+ "sync/atomic"
27+ "time"
28+
29+ "github.com/go-spring/spring-base/util"
2330)
2431
2532// Stdout is the standard output stream used by appenders.
@@ -45,10 +52,10 @@ type Appender interface {
4552}
4653
4754var (
55+ _ Appender = (* GroupAppender )(nil )
4856 _ Appender = (* DiscardAppender )(nil )
4957 _ Appender = (* ConsoleAppender )(nil )
5058 _ Appender = (* FileAppender )(nil )
51- _ Appender = (* GroupAppender )(nil )
5259)
5360
5461// AppenderBase provides common configuration fields for all appenders.
@@ -66,6 +73,37 @@ func (c *AppenderBase) EnableLevel(level Level) bool {
6673 return level .code >= c .MinLevel .code && level .code <= c .MaxLevel .code
6774}
6875
76+ // AppenderRef represents a reference to an appender by name.
77+ // The actual Appender is resolved and injected later during configuration.
78+ type AppenderRef struct {
79+ Appender
80+ Ref string `PluginAttribute:"ref"`
81+ }
82+
83+ // GroupAppender forwards log events to a group of other appenders.
84+ type GroupAppender struct {
85+ AppenderBase
86+ AppenderRefs []* AppenderRef `PluginElement:"AppenderRef"`
87+ }
88+
89+ func (c * GroupAppender ) Start () error { return nil }
90+ func (c * GroupAppender ) Stop () {}
91+ func (c * GroupAppender ) ConcurrentSafe () bool { return true }
92+
93+ // Append forwards the event to each child appender.
94+ func (c * GroupAppender ) Append (e * Event ) {
95+ for _ , r := range c .AppenderRefs {
96+ r .Append (e )
97+ }
98+ }
99+
100+ // Write forwards raw bytes to each child appender.
101+ func (c * GroupAppender ) Write (b []byte ) {
102+ for _ , r := range c .AppenderRefs {
103+ r .Write (b )
104+ }
105+ }
106+
69107// DiscardAppender ignores all log events (no-op).
70108type DiscardAppender struct {
71109 AppenderBase
@@ -143,33 +181,185 @@ func (c *FileAppender) Stop() {
143181 }
144182}
145183
146- // AppenderRef represents a reference to an appender by name.
147- // The actual Appender is resolved and injected later during configuration.
148- type AppenderRef struct {
149- Appender
150- Ref string `PluginAttribute:"ref"`
184+ func init () {
185+ // register built-in converters
186+ RegisterConverter (ParseRotateStrategy )
187+
188+ // register built-in rotate strategies
189+ RegisterRotateStrategy ("1h" , FixedRotateStrategy {Interval : time .Hour })
190+ RegisterRotateStrategy ("30m" , FixedRotateStrategy {Interval : 30 * time .Minute })
191+ RegisterRotateStrategy ("10m" , FixedRotateStrategy {Interval : 10 * time .Minute })
151192}
152193
153- // GroupAppender forwards log events to a group of other appenders.
154- type GroupAppender struct {
155- AppenderBase
156- AppenderRefs []* AppenderRef `PluginElement:"AppenderRef"`
194+ var rotateStrategyRegistry = map [string ]RotateStrategy {}
195+
196+ // RotateStrategy defines the interface for log rotation strategies.
197+ type RotateStrategy interface {
198+ Time (t time.Time ) int64
199+ Format (t time.Time ) string
157200}
158201
159- func (c * GroupAppender ) Start () error { return nil }
160- func (c * GroupAppender ) Stop () {}
161- func (c * GroupAppender ) ConcurrentSafe () bool { return true }
202+ // FixedRotateStrategy represents a fixed-interval rotation strategy.
203+ type FixedRotateStrategy struct {
204+ Interval time.Duration // The rotation interval duration.
205+ }
162206
163- // Append forwards the event to each child appender.
164- func (c * GroupAppender ) Append (e * Event ) {
165- for _ , r := range c .AppenderRefs {
166- r .Append (e )
207+ // Time returns the timestamp aligned to the nearest previous rotation point.
208+ func (r FixedRotateStrategy ) Time (t time.Time ) int64 {
209+ seconds := int64 (r .Interval .Seconds ())
210+ return (t .Unix () / seconds ) * seconds
211+ }
212+
213+ // Format formats the time into a string with the pattern "yyyyMMddHHmmss".
214+ func (r FixedRotateStrategy ) Format (t time.Time ) string {
215+ return t .Format ("20060102150405" )
216+ }
217+
218+ // RegisterRotateStrategy registers a rotation strategy with a given name.
219+ func RegisterRotateStrategy (name string , strategy RotateStrategy ) {
220+ rotateStrategyRegistry [name ] = strategy
221+ }
222+
223+ // ParseRotateStrategy retrieves a registered rotation strategy by name.
224+ func ParseRotateStrategy (name string ) (RotateStrategy , error ) {
225+ s , ok := rotateStrategyRegistry [name ]
226+ if ! ok {
227+ return nil , fmt .Errorf ("invalid rotate strategy: %q" , name )
167228 }
229+ return s , nil
168230}
169231
170- // Write forwards raw bytes to each child appender.
171- func (c * GroupAppender ) Write (b []byte ) {
172- for _ , r := range c .AppenderRefs {
173- r .Write (b )
232+ // RotateFileAppender allows **multiple goroutines** to call Write()
233+ // safely, at the cost of slightly higher overhead and potential
234+ // (acceptable) log loss during rotation.
235+ //
236+ // Usage scenarios:
237+ // - High-concurrency applications where logs may be produced
238+ // from many goroutines.
239+ //
240+ // Risks:
241+ // - During rotation, a small number of writes may fail if they
242+ // occur after the old file is closed but before the new file is ready.
243+ // - During Stop(), concurrent writes may also be lost.
244+ // - If zero log loss is required, use AsyncRotateFileWriter
245+ // with a dedicated logging goroutine instead.
246+ type RotateFileAppender struct {
247+ FileDir string
248+ FileName string
249+ ClearHours int32
250+ RotateStrategy RotateStrategy
251+ file atomic.Pointer [os.File ]
252+ mutex sync.Mutex
253+ currTime atomic.Int64
254+ }
255+
256+ // Start opens the initial log file.
257+ func (c * RotateFileAppender ) Start () error {
258+ now := time .Now ()
259+ filePath , file , err := c .createFile (now )
260+ if err != nil {
261+ return util .WrapError (err , "Failed to create log file %s" , filePath )
262+ }
263+ c .file .Store (file )
264+ c .currTime .Store (c .RotateStrategy .Time (now ))
265+ return nil
266+ }
267+
268+ // Write writes bytes to the current log file.
269+ // May lose a few writes during rotation or Stop().
270+ func (c * RotateFileAppender ) Write (b []byte ) {
271+ c .rotate ()
272+ if file := c .file .Load (); file != nil {
273+ _ , _ = file .Write (b )
274+ }
275+ }
276+
277+ // Stop flushes and closes the current file.
278+ func (c * RotateFileAppender ) Stop () {
279+ c .rotate ()
280+ if file := c .file .Swap (nil ); file != nil {
281+ _ = file .Sync ()
282+ _ = file .Close ()
283+ }
284+ }
285+
286+ // rotate checks if the current time has passed into a new rotation slot.
287+ // If so, it closes the old file, opens a new one, and triggers cleanup.
288+ // Risk: If file creation fails during rotation, new logs will be lost
289+ // until the issue is resolved.
290+ func (c * RotateFileAppender ) rotate () {
291+ now := time .Now ()
292+ nowTime := c .RotateStrategy .Time (now )
293+ if nowTime <= c .currTime .Load () {
294+ return // still in the current slot
295+ }
296+
297+ // serialize rotation
298+ c .mutex .Lock ()
299+ defer c .mutex .Unlock ()
300+
301+ // double-check after acquiring the lock
302+ if nowTime <= c .currTime .Load () {
303+ return
304+ }
305+
306+ // close the old file
307+ if file := c .file .Load (); file != nil {
308+ _ = file .Sync ()
309+ _ = file .Close ()
310+ }
311+
312+ filePath , file , err := c .createFile (now )
313+ if err != nil {
314+ err = util .WrapError (err , "Failed to create log file %s" , filePath )
315+ _ , _ = fmt .Fprintln (os .Stderr , err )
316+ c .file .Store (nil )
317+ return
318+ }
319+ c .file .Store (file )
320+ c .currTime .Store (nowTime )
321+
322+ // trigger cleanup after each rotation for timely housekeeping
323+ go c .clearExpiredFiles ()
324+ }
325+
326+ func (c * RotateFileAppender ) ConcurrentSafe () bool { return true }
327+
328+ func (c * RotateFileAppender ) Append (e * Event ) {
329+ panic (util .ErrForbiddenMethod )
330+ }
331+
332+ // createFile creates or opens the current log file for appending.
333+ // The application is responsible for ensuring the directory exists.
334+ func (c * RotateFileAppender ) createFile (t time.Time ) (string , * os.File , error ) {
335+ fileName := c .FileName + "." + c .RotateStrategy .Format (t )
336+ filePath := filepath .Join (c .FileDir , fileName )
337+ const fileFlag = os .O_CREATE | os .O_WRONLY | os .O_APPEND
338+ file , err := os .OpenFile (filePath , fileFlag , 0644 )
339+ if err != nil {
340+ return filePath , nil , err
341+ }
342+ return filePath , file , nil
343+ }
344+
345+ // clearExpiredFiles removes expired log files.
346+ func (c * RotateFileAppender ) clearExpiredFiles () {
347+ expiration := time .Now ().Add (- time .Duration (c .ClearHours ) * time .Hour )
348+ entries , _ := os .ReadDir (c .FileDir )
349+ for _ , entry := range entries {
350+ if entry .IsDir () {
351+ continue
352+ }
353+ if ! strings .HasPrefix (entry .Name (), c .FileName + "." ) {
354+ continue
355+ }
356+ info , err := entry .Info ()
357+ if err != nil {
358+ continue
359+ }
360+ if info .ModTime ().Before (expiration ) {
361+ filePath := fmt .Sprintf ("%s/%s" , c .FileDir , entry .Name ())
362+ _ = os .Remove (filePath )
363+ }
174364 }
175365}
0 commit comments