feat: 新增 survey 包,包含了运营日志的基本功能实现

This commit is contained in:
kercylan98 2023-08-22 19:34:53 +08:00
parent 9740dfd46e
commit e962009eff
6 changed files with 245 additions and 0 deletions

View File

@ -0,0 +1,68 @@
package survey
import (
"bufio"
"github.com/kercylan98/minotaur/utils/super"
"io"
"os"
"time"
)
// All 处理特定记录器特定日期的所有记录,当 handle 返回 false 时停止处理
func All(name string, t time.Time, handle func(record map[string]any) bool) {
logger := survey[name]
if logger == nil {
return
}
fp := logger.filePath(t.Format(logger.layout))
logger.wl.Lock()
defer logger.wl.Unlock()
f, err := os.Open(fp)
if err != nil {
return
}
defer func() {
_ = f.Close()
}()
reader := bufio.NewReader(f)
var m = make(map[string]any)
for {
line, _, err := reader.ReadLine()
if err == io.EOF {
break
}
if err != nil {
panic(err)
}
if err = super.UnmarshalJSON(line[logger.dataLayoutLen:], &m); err != nil {
panic(err)
}
if !handle(m) {
break
}
}
}
// Sum 处理特定记录器特定日期的所有记录,根据指定的字段进行汇总
func Sum(name string, t time.Time, field string) float64 {
var res float64
All(name, t, func(record map[string]any) bool {
v, exist := record[field]
if !exist {
return true
}
switch value := v.(type) {
case float64:
res += value
case int:
res += float64(value)
case int64:
res += float64(value)
case string:
res += super.StringToFloat64(value)
}
return true
})
return res
}

View File

@ -0,0 +1,86 @@
package survey
import (
"bufio"
"fmt"
"github.com/kercylan98/minotaur/utils/log"
"os"
"path/filepath"
"sync"
)
const (
DATETIME_FORMAT = "2006-01-02 15:04:05"
DATE_FORMAT = "2006-01-02"
dateLen = len(DATE_FORMAT)
datetimeLen = len(DATETIME_FORMAT)
)
// logger 用于埋点数据的运营日志记录器
type logger struct {
bl sync.Mutex // writer lock
wl sync.Mutex // flush lock
dir string
fn string
fe string
bs []string
layout string
layoutLen int
dataLayout string
dataLayoutLen int
}
// flush 将记录器缓冲区的数据写入到文件
func (slf *logger) flush() {
slf.bl.Lock()
count := len(slf.bs)
if count == 0 {
slf.bl.Unlock()
return
}
ds := slf.bs[:]
slf.bs = slf.bs[count:]
slf.bl.Unlock()
slf.wl.Lock()
defer slf.wl.Unlock()
var (
file *os.File
writer *bufio.Writer
err error
last string
)
for _, data := range ds {
tick := data[0:slf.layoutLen]
if tick != last {
if file != nil {
_ = writer.Flush()
_ = file.Close()
}
fp := slf.filePath(tick)
file, err = os.OpenFile(fp, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
log.Fatal("Survey", log.String("Action", "DateSwitch"), log.String("FilePath", fp), log.Err(err))
return
}
writer = bufio.NewWriterSize(file, 1024*10240)
last = tick
}
_, _ = writer.WriteString(data)
}
_ = writer.Flush()
_ = file.Close()
}
// writer 写入数据到记录器缓冲区
func (slf *logger) writer(d string) {
slf.bl.Lock()
slf.bs = append(slf.bs, d)
slf.bl.Unlock()
}
// filePath 获取文件路径
func (slf *logger) filePath(t string) string {
return filepath.Join(slf.dir, fmt.Sprintf("%s.%s%s", slf.fn, t, slf.fe))
}

View File

@ -0,0 +1,13 @@
package survey
// Option 选项
type Option func(logger *logger)
// WithLayout 设置日志文件名的时间戳格式
// - 默认为 time.DateOnly
func WithLayout(layout string) Option {
return func(logger *logger) {
logger.layout = layout
logger.layoutLen = len(layout)
}
}

View File

@ -0,0 +1,56 @@
package survey
import (
"fmt"
"github.com/kercylan98/minotaur/utils/log"
"github.com/kercylan98/minotaur/utils/super"
"path/filepath"
"strings"
"time"
)
var (
survey = make(map[string]*logger)
)
// RegSurvey 注册一个运营日志记录器
func RegSurvey(name, filePath string, options ...Option) {
fn := filepath.Base(filePath)
ext := filepath.Ext(fn)
fn = strings.TrimSuffix(fn, ext)
_, exist := survey[name]
if exist {
panic(fmt.Errorf("survey %s already exist", name))
}
dir := filepath.Dir(filePath)
logger := &logger{
dir: dir,
fn: fn,
fe: ext,
layout: time.DateOnly,
layoutLen: len(time.DateOnly),
dataLayout: time.DateTime,
dataLayoutLen: len(time.DateTime) + 3,
}
for _, option := range options {
option(logger)
}
survey[name] = logger
log.Info("Survey", log.String("Action", "RegSurvey"), log.String("Name", name), log.String("FilePath", dir+"/"+fn+".${DATE}"+ext))
}
// Record 记录一条运营日志
func Record(name string, data map[string]any) {
logger, exist := survey[name]
if !exist {
panic(fmt.Errorf("survey %s not exist", name))
}
logger.writer(fmt.Sprintf("%s - %s\n", time.Now().Format(time.DateTime), super.MarshalJSON(data)))
}
// Flush 将所有运营日志记录器的缓冲区数据写入到文件
func Flush() {
for _, logger := range survey {
logger.flush()
}
}

View File

@ -0,0 +1,21 @@
package survey_test
import (
"fmt"
"github.com/kercylan98/minotaur/utils/log/survey"
"os"
"testing"
"time"
)
func TestRecord(t *testing.T) {
_ = os.MkdirAll("./test", os.ModePerm)
survey.RegSurvey("GLOBAL_DATA", "./test/global_data.log")
survey.Record("GLOBAL_DATA", map[string]any{
"joinTime": time.Now().Unix(),
"action": 1,
})
survey.Flush()
fmt.Println(survey.Sum("GLOBAL_DATA", time.Now(), "action"))
}

View File

@ -0,0 +1 @@
2023-08-22 19:34:15 - {"action":1,"joinTime":1692704055}