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  }