Skip to content

Commit 301cde8

Browse files
lvan100lianghuan
authored andcommitted
111
1 parent 8cae28d commit 301cde8

File tree

2 files changed

+211
-259
lines changed

2 files changed

+211
-259
lines changed

plugin_appender.go

Lines changed: 211 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,16 @@
1717
package log
1818

1919
import (
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

4754
var (
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).
70108
type 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

Comments
 (0)