github.com/bingoohuang/gg@v0.0.0-20240325092523-45da7dee9335/pkg/rotate/queue.go (about) 1 package rotate 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "log" 9 "os" 10 "regexp" 11 "strings" 12 "sync" 13 "sync/atomic" 14 "time" 15 16 "github.com/bingoohuang/gg/pkg/delay" 17 "github.com/bingoohuang/gg/pkg/iox" 18 "github.com/bingoohuang/gg/pkg/man" 19 "github.com/bingoohuang/gg/pkg/ss" 20 ) 21 22 // QueueWriter output parsed http messages 23 type QueueWriter struct { 24 queue chan string 25 writer io.Writer 26 27 discarded uint32 28 config *Config 29 delayDiscarded *delay.Chan 30 31 wg sync.WaitGroup 32 } 33 34 type Config struct { 35 context.Context 36 OutChanSize int // 通道大小 37 AllowDiscarded bool // 是否允许放弃(来不及写入) 38 Append bool // 追加模式 39 MaxSize uint64 // 单个文件最大大小 40 KeepDays int // 保留多少天的日志,过期删除, 0全部, 默认10天 41 FlushLatency time.Duration // 刷新延迟 42 } 43 44 type Option func(*Config) 45 46 func WithContext(v context.Context) Option { return func(c *Config) { c.Context = v } } 47 func WithConfig(v *Config) Option { return func(c *Config) { *c = *v } } 48 func WithAllowDiscard(v bool) Option { return func(c *Config) { c.AllowDiscarded = v } } 49 func WithAppend(v bool) Option { return func(c *Config) { c.Append = v } } 50 func WithOutChanSize(v int) Option { return func(c *Config) { c.OutChanSize = v } } 51 func WithMaxSize(v uint64) Option { return func(c *Config) { c.MaxSize = v } } 52 func WithKeepDays(v int) Option { return func(c *Config) { c.KeepDays = v } } 53 func WithFlushLatency(v time.Duration) Option { return func(c *Config) { c.FlushLatency = v } } 54 55 // NewQueueWriter creates a new QueueWriter. 56 // outputPath: 57 // 1. stdout for the stdout 58 // 2. somepath/yyyyMMdd.log for the disk file 59 // 2.1. somepath/yyyyMMdd.log:append for the disk file for append mode 60 // 2.2. somepath/yyyyMMdd.log:100m for the disk file max 100MB size 61 // 2.3. somepath/yyyyMMdd.log:100m:append for the disk file max 100MB size and append mode 62 func NewQueueWriter(outputPath string, options ...Option) *QueueWriter { 63 c := createConfig(options) 64 s := ParseOutputPath(c, outputPath) 65 w := createWriter(s, c) 66 p := &QueueWriter{ 67 queue: make(chan string, c.OutChanSize), 68 writer: &iox.MaxLatencyWriter{Dst: w, Latency: c.FlushLatency}, 69 config: c, 70 } 71 72 if c.AllowDiscarded { 73 p.delayDiscarded = delay.NewChan(c.Context, func(_, v interface{}) { 74 p.Send(fmt.Sprintf("\n discarded: %d\n", v.(uint32)), false) 75 }, c.FlushLatency) 76 } 77 p.wg.Add(1) 78 go p.flushing() 79 80 return p 81 } 82 83 func createConfig(options []Option) *Config { 84 c := &Config{KeepDays: 10, FlushLatency: 10 * time.Second} 85 for _, option := range options { 86 option(c) 87 } 88 89 if c.OutChanSize <= 0 { 90 c.OutChanSize = 1000 91 } 92 93 if c.Context == nil { 94 c.Context = context.Background() 95 } 96 97 return c 98 } 99 100 var digits = regexp.MustCompile(`^\d+$`) 101 102 func ParseOutputPath(c *Config, outputPath string) string { 103 if outputPath == "stdout" || outputPath == "stderr" || strings.HasPrefix(outputPath, "stdout:") || strings.HasPrefix(outputPath, "stderr:") { 104 return outputPath 105 } 106 107 s := ss.RemoveAll(outputPath, ":append") 108 if s != outputPath { 109 c.Append = true 110 } 111 112 if pos := strings.LastIndex(s, ":"); pos > 0 { 113 if !digits.MatchString(s[pos+1:]) { 114 maxSize, _ := man.ParseBytes(s[pos+1:]) 115 if maxSize > 0 { 116 c.MaxSize = maxSize 117 } 118 s = s[:pos] 119 } 120 } 121 return s 122 } 123 124 type LfLog struct{} 125 126 func (l LfLog) Flush() error { return nil } 127 128 func (l LfLog) Write(p []byte) (n int, err error) { 129 log.Printf("[PRE]%s", bytes.TrimSpace(p)) 130 return len(p), nil 131 } 132 133 type LfStdout struct { 134 io.Writer 135 } 136 137 func (l LfStdout) Flush() error { return nil } 138 139 func (l LfStdout) Write(p []byte) (n int, err error) { 140 return fmt.Fprintf(l.Writer, "%s\n", bytes.TrimSpace(p)) 141 } 142 143 func createWriter(outputPath string, c *Config) iox.WriteFlusher { 144 switch { 145 case outputPath == "stdout": 146 return &LfStdout{Writer: os.Stdout} 147 case outputPath == "stderr": 148 return &LfStdout{Writer: os.Stderr} 149 case strings.HasPrefix(outputPath, "stdout:"): 150 if option := strings.TrimPrefix(outputPath, "stdout:"); option == "log" { 151 return &LfLog{} 152 } else { 153 return &LfStdout{Writer: os.Stdout} 154 } 155 case strings.HasPrefix(outputPath, "stderr:"): 156 if option := strings.TrimPrefix(outputPath, "stdout:"); option == "log" { 157 return &LfLog{} 158 } else { 159 return &LfStdout{Writer: os.Stderr} 160 } 161 default: 162 return NewFileWriter(outputPath, c.MaxSize, c.Append, c.KeepDays) 163 } 164 } 165 166 func (p *QueueWriter) Send(msg string, countDiscards bool) { 167 if msg == "" { 168 return 169 } 170 171 defer func() { 172 if err := recover(); err != nil { 173 log.Printf("W! Recovered %v", err) 174 } 175 }() // avoid write to closed p.queue 176 177 if !p.config.AllowDiscarded { 178 p.queue <- msg 179 return 180 } 181 182 select { 183 case p.queue <- msg: 184 default: 185 if countDiscards { 186 p.delayDiscarded.Put(atomic.AddUint32(&p.discarded, 1)) 187 } 188 } 189 } 190 191 func (p *QueueWriter) flushing() { 192 defer p.wg.Done() 193 if c, ok := p.writer.(io.Closer); ok { 194 defer c.Close() 195 } 196 197 ctx := p.config.Context 198 for { 199 select { 200 case msg, ok := <-p.queue: 201 if !ok { 202 return 203 } 204 _, _ = p.writer.Write([]byte(msg)) 205 case <-ctx.Done(): 206 return 207 } 208 } 209 } 210 211 func (p *QueueWriter) daysKeeping() { 212 ticker := time.NewTicker(24 * time.Hour) 213 defer ticker.Stop() 214 ctx := p.config.Context 215 216 for { 217 select { 218 case <-ticker.C: 219 p.removeExpiredFiles() 220 case <-ctx.Done(): 221 return 222 } 223 } 224 } 225 226 func (p *QueueWriter) Close() error { 227 if p.config.AllowDiscarded { 228 if val := atomic.LoadUint32(&p.discarded); val > 0 { 229 p.queue <- fmt.Sprintf("\n#%d discarded", val) 230 } 231 } 232 close(p.queue) 233 p.wg.Wait() 234 return nil 235 } 236 237 func (p *QueueWriter) removeExpiredFiles() { 238 }