diff --git a/src/server/conf/json.go b/src/server/conf/json.go index 936a4f4c..471018f0 100644 --- a/src/server/conf/json.go +++ b/src/server/conf/json.go @@ -32,6 +32,11 @@ var Server struct { RedisPwd string RedisDb int + RedisWriteAddr string // 主写地址(host:port 或 单独 host, 仍兼容旧 RedisAddr/RedisPort) + RedisReadAddrs string // 只读地址,逗号分隔(host:port,...) + RedisMasterName string // 哨兵模式下的 master 名称 + RedisConnType string // "Direct" 或 "Sentinel" + GameName string ServerType string diff --git a/src/server/conf/server.json b/src/server/conf/server.json index 4f0f78d4..e139badd 100644 --- a/src/server/conf/server.json +++ b/src/server/conf/server.json @@ -25,9 +25,15 @@ "ServerCenter" : 1, "GameConfPath": "D:/Github/pet_home_server/src/server/gamedata/config/", - "RedisAddr":"127.0.0.1", - "RedisPort" :"6379", + "RedisAddr":"127.0.0.1", + "RedisPort" :"6379", "RedisPwd" :"", + + "RedisWriteAddr":"127.0.0.1:6379", + "RedisReadAddrs":"127.0.0.1:6379", + "RedisMasterName":"mymaster", + "RedisConnType":"Direct", + "GoogleVerify":false, "RemoteAddr":"host.docker.internal:9001", "Partition":3, diff --git a/src/server/db/Redis.go b/src/server/db/Redis.go index 820aa2e9..f8f59896 100644 --- a/src/server/db/Redis.go +++ b/src/server/db/Redis.go @@ -4,7 +4,7 @@ import ( "context" "server/conf" "server/pkg/github.com/name5566/leaf/log" - "sync" + "strings" "time" "github.com/redis/go-redis/v9" @@ -12,15 +12,21 @@ import ( var ctx = context.Background() -var redisMu sync.Mutex +var RdbWrite *redis.Client +var RdbRead *redis.Client -var Rdb *redis.Client - -// 封装创建连接 -func connectRedis() (*redis.Client, error) { +// helper: 创建单个客户端(addr 可以为 host:port 或 host) +func connectClient(addr string) (*redis.Client, error) { + if addr == "" { + return nil, nil + } + // 如果没有端口且配置了旧的 RedisPort,则尝试补端口 + if !strings.Contains(addr, ":") && conf.Server.RedisPort != "" { + addr = addr + ":" + conf.Server.RedisPort + } rdb := redis.NewClient(&redis.Options{ - Addr: conf.Server.RedisAddr + ":" + conf.Server.RedisPort, - Password: conf.Server.RedisPwd, // no password set + Addr: addr, + Password: conf.Server.RedisPwd, DB: conf.Server.RedisDb, }) if _, err := rdb.Ping(ctx).Result(); err != nil { @@ -29,57 +35,95 @@ func connectRedis() (*redis.Client, error) { return rdb, nil } +// InitRedis: 初始化读写分离客户端(向后兼容旧配置) func InitRedis() { - rdb, err := connectRedis() - if err != nil { - log.Debug("连接redis出错,错误信息:%v", err) - return - } - Rdb = rdb - log.Debug("成功连接redis") - - // 定时检测与重连 - go func() { - ticker := time.NewTicker(time.Minute) - defer ticker.Stop() - for range ticker.C { - redisMu.Lock() - cur := Rdb - redisMu.Unlock() - if cur == nil || cur.Ping(ctx).Err() != nil { - log.Debug("redis ping failed, start reconnect") - ReconnectRedis() + // 决定写地址:优先使用 RedisWriteAddr,其次使用旧的 RedisAddr:RedisPort + writeAddr := conf.Server.RedisWriteAddr + if writeAddr == "" { + if conf.Server.RedisAddr != "" { + writeAddr = conf.Server.RedisAddr + if conf.Server.RedisPort != "" && !strings.Contains(writeAddr, ":") { + writeAddr = writeAddr + ":" + conf.Server.RedisPort } } - }() -} + } -// 重连 -func ReconnectRedis() { - redisMu.Lock() - defer redisMu.Unlock() - newRdb, err := connectRedis() + // 决定读地址:优先使用 RedisReadAddrs(逗号分隔),若为空则回退到写地址 + readAddrs := conf.Server.RedisReadAddrs + if strings.TrimSpace(readAddrs) == "" { + readAddrs = writeAddr + } + + // 取第一个可用的只读地址(简单实现) + var readClient *redis.Client + for _, a := range strings.Split(readAddrs, ",") { + a = strings.TrimSpace(a) + if a == "" { + continue + } + c, err := connectClient(a) + if err == nil { + readClient = c + break + } + log.Debug("connect read addr %s failed: %v", a, err) + } + + // 如果所有只读都不可用,尝试连接写地址作为回退 + writeClient, err := connectClient(writeAddr) if err != nil { - log.Debug("redis reconnect failed: %v", err) + log.Debug("连接redis写节点出错,错误信息:%v", err) + // 若读已连上则也作为写回退,否则返回 + if readClient != nil { + RdbWrite = readClient + RdbRead = readClient + log.Debug("只有只读节点可用,读写共用该节点") + return + } return } - if Rdb != nil { - _ = Rdb.Close() + + // 如果读未连接成功,读回退到写 + if readClient == nil { + readClient = writeClient } - Rdb = newRdb - log.Debug("redis reconnect success") + + RdbWrite = writeClient + RdbRead = readClient + log.Debug("成功初始化 redis(读写分离),写: %v, 读: %v", writeAddr, readAddrs) } +// 写操作使用 RdbWrite func RedisSetKey(key string, value string, expiration time.Duration) { - err := Rdb.Set(ctx, key, value, expiration).Err() + if RdbWrite == nil { + log.Debug("redis write client is nil") + return + } + err := RdbWrite.Set(ctx, key, value, expiration).Err() if err != nil { log.Debug("redis set failed, err:%v\n", err) } } -// 获取锁 +// 新增:写入字节数据,避免 string 转换拷贝 +func RedisSetKeyBytes(key string, value []byte, expiration time.Duration) { + if RdbWrite == nil { + log.Debug("redis write client is nil") + return + } + err := RdbWrite.Set(ctx, key, value, expiration).Err() + if err != nil { + log.Debug("redis set failed, err:%v\n", err) + } +} + +// 获取锁(写) func RedisLock(key string, value string, expiration time.Duration) bool { - ok, err := Rdb.SetNX(ctx, key, value, expiration).Result() + if RdbWrite == nil { + log.Debug("redis write client is nil") + return false + } + ok, err := RdbWrite.SetNX(ctx, key, value, expiration).Result() if err != nil { log.Debug("redis lock failed, err:%v\n", err) return false @@ -87,8 +131,12 @@ func RedisLock(key string, value string, expiration time.Duration) bool { return ok } -// 释放锁 +// 释放锁(写) func RedisUnlock(key string, value string) bool { + if RdbWrite == nil { + log.Debug("redis write client is nil") + return false + } script := ` if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) @@ -96,7 +144,7 @@ func RedisUnlock(key string, value string) bool { return 0 end ` - result, err := Rdb.Eval(ctx, script, []string{key}, value).Result() + result, err := RdbWrite.Eval(ctx, script, []string{key}, value).Result() if err != nil { log.Debug("redis unlock failed, err:%v\n", err) return false @@ -104,8 +152,12 @@ func RedisUnlock(key string, value string) bool { return result.(int64) == 1 } +// 读操作使用 RdbRead func RedisGetKey(key string) (string, error) { - val, err := Rdb.Get(ctx, key).Result() + if RdbRead == nil { + return "", nil + } + val, err := RdbRead.Get(ctx, key).Result() if err != nil { return "", err } @@ -113,21 +165,32 @@ func RedisGetKey(key string) (string, error) { } func RedisDelKey(key string) { - err := Rdb.Del(ctx, key).Err() + if RdbWrite == nil { + log.Debug("redis write client is nil") + return + } + err := RdbWrite.Del(ctx, key).Err() if err != nil { log.Debug("redis del failed, err:%v\n", err) } } func RedisZAdd(key string, member string, score float64) { - err := Rdb.ZAdd(ctx, key, redis.Z{Score: score, Member: member}).Err() + if RdbWrite == nil { + log.Debug("redis write client is nil") + return + } + err := RdbWrite.ZAdd(ctx, key, redis.Z{Score: score, Member: member}).Err() if err != nil { log.Debug("redis zadd failed, err:%v\n", err) } } func RedisZRangeWithScores(key string, start, stop int64) ([]redis.Z, error) { - val, err := Rdb.ZRangeWithScores(ctx, key, start, stop).Result() + if RdbRead == nil { + return nil, nil + } + val, err := RdbRead.ZRangeWithScores(ctx, key, start, stop).Result() if err != nil { return nil, err } @@ -135,7 +198,10 @@ func RedisZRangeWithScores(key string, start, stop int64) ([]redis.Z, error) { } func RedisZRevRangeWithScores(key string, start, stop int64) ([]redis.Z, error) { - val, err := Rdb.ZRevRangeWithScores(ctx, key, start, stop).Result() + if RdbRead == nil { + return nil, nil + } + val, err := RdbRead.ZRevRangeWithScores(ctx, key, start, stop).Result() if err != nil { return nil, err } @@ -143,11 +209,14 @@ func RedisZRevRangeWithScores(key string, start, stop int64) ([]redis.Z, error) } func RedisZRankWithScores(key, member string) (int64, float64, error) { - val, err := Rdb.ZRank(ctx, key, member).Result() + if RdbRead == nil { + return 0, 0, nil + } + val, err := RdbRead.ZRank(ctx, key, member).Result() if err != nil { return 0, 0, err } - score, err := Rdb.ZScore(ctx, key, member).Result() + score, err := RdbRead.ZScore(ctx, key, member).Result() if err != nil { return 0, 0, err } @@ -155,7 +224,11 @@ func RedisZRankWithScores(key, member string) (int64, float64, error) { } func RedisDel(key string) { - err := Rdb.Del(ctx, key).Err() + if RdbWrite == nil { + log.Debug("redis write client is nil") + return + } + err := RdbWrite.Del(ctx, key).Err() if err != nil { log.Debug("redis del failed, err:%v\n", err) } diff --git a/src/server/ga/log.go b/src/server/ga/log.go new file mode 100644 index 00000000..340020d3 --- /dev/null +++ b/src/server/ga/log.go @@ -0,0 +1,29 @@ +package ga + +import ( + galog "github.com/tuyou/galog" +) + +const ( + PROJECT_ID = "20659" + CLIENT_ID = "Android_5.00_tyGuest,facebook.googleplay.0-hall20659.googleplay.Meowment" +) + +var glogger *galog.GALogger + +func init() { + glog, err := galog.NewGALogger("logs", PROJECT_ID, CLIENT_ID, galog.LogTypeTrack) + if err != nil { + panic(err) + } + glogger = glog +} + +func GAlogEvent(event string, userID string, deviceID string, properties map[string]interface{}) { + glogger. + GetEntry(event). + SetDeviceID(deviceID). + SetUserID(userID). + SetProperties(properties). + Flush() +} diff --git a/src/server/galog/.gitignore b/src/server/galog/.gitignore new file mode 100644 index 00000000..e3c5d3e4 --- /dev/null +++ b/src/server/galog/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +.vscode/ +.idea/ +*.log +logs/ +ga_log/ +asset_log/ \ No newline at end of file diff --git a/src/server/galog/README.md b/src/server/galog/README.md new file mode 100644 index 00000000..70e08d0b --- /dev/null +++ b/src/server/galog/README.md @@ -0,0 +1,292 @@ +# galog + +galog是一个无三方依赖、支持单CPU最高5wQPS写入的ga-sdk组件,可直接用于GA事实日志和资产日志打点。 + +> 说明:默认是异步写入模式,异步队列长度=100,较大QPS时可自行调整,详见参数: +> - enableAsync +> - defaultAsyncMsgLen + +## Usage + +```golang +// ga日志初始化 +projectID := "20433" +clientID := "Android_5.0_tyGuest,weixinPay,tyAccount.alipay.0-hall20433.tuyoo.sdktest" +glogger, err := galog.NewGALogger("logs", projectID, clientID, galog.LogTypeTrack) +if err != nil { + panic(err) +} + +// ga日志打点调用 +props := map[string]interface{}{ + "ip_address": "127.0.0.1", + "proj_app_id": "10010", + "uuid": "4951d472-2c46-4fe5-9c4f-c35b6fb53f67", + "ts": tt.UnixNano(), +} +glogger. + GetEntry("sdk_s_login_succ"). + SetDeviceID("device001"). + SetUserID("10086"). + SetProperties(props). + Flush() +``` + +```golang +// asset日志初始化 +projectID := "28" +clientID := "Android_4.827_tyGuest,nearme.nearme.0-hall28.oppo.bydzz" +glogger, err := galog.NewGALogger("logs", projectID, clientID, galog.LogTypeAsset) +if err != nil { + panic(err) +} + +// asset日志打点调用 +asset := galog.AssetProperties{} +asset. + SetAssetID("13101"). + SetAssetName("\u9501\u5b9a"). + SetAssetType("6"). + SetAssetFinal("2"). + SetAssetAssociated("3"). + SetAssetStartTime("0"). + SetAssetTimeLimit("0"). + SetAssetSource(""). + SetKV("uuid", "uuid-v4") + +glogger. + GetEntry("asset_increase"). + SetDeviceID(""). + SetUserID("10086"). + SetProperties(asset). + Flush() +``` + +## suger usage + +```golang +package chat_log + +import ( + "log" + "strconv" + + "tygit.tuyoo.com/gocomponents/galog" +) + +var ( + slogger *galog.Logger + galogger *galog.GALogger + + lerr error +) + +// MustInitLoggerOnce 服务启动后初始化一次 +func MustInitLoggerOnce() { + slogger, lerr = galog.NewServerLogger(&galog.ServerLogOptions{ + LogDir: "run/logs", + EnableAsync: true, + AsyncQueueSize: 1000, + }) + if lerr != nil { + log.Printf("MustInitLoggerOnce err: %s\n", lerr.Error()) + panic("MustInitLoggerOnce err") + } +} + +// MustInitGALoggerOnce 服务启动后初始化一次 +func MustInitGALoggerOnce() { + galogger, lerr = galog.NewServerGALogger(&galog.ServerGALogOptions{ + LogDir: "run/bi", + EnableAsync: true, + AsyncQueueSize: 1000, + ProjectID: ProjectId, + ClientID: ClientId, + LogType: galog.LogTypeTrack, + }) + if lerr != nil { + log.Printf("MustInitGALoggerOnce err: %s\n", lerr.Error()) + panic("MustInitGALoggerOnce err") + } +} + +func LogToGANew(eventName, deviceId string, userId int64, prop map[string]interface{}, version string) { + lib := map[string]string{ + "lib_type": "golang", + "lib_version": version, + } + + galogger. + GetEntry(eventName). + SetDeviceID(deviceId). + SetUserID(strconv.FormatInt(userId, 10)). + SetLib(lib). + SetProperties(prop). + Flush() +} +``` + +## 日志目录结构 + +``` +{logDir}/ga_log/{日志类型}_{hostname}_{年月日}_{小时}.log +{logDir}/asset_log/{日志类型}_{hostname}_{年月日}_{小时}.log +``` + +## Benchmark + +- 测试机:Mac-M1 8c 16G + +- 同步写入模式 +``` +goos: darwin +goarch: arm64 +pkg: tygit.tuyoo.com/gocomponents/galog +BenchmarkGaLog-8 87565 12831 ns/op 7590 B/op 90 allocs/op +BenchmarkAssetLog-8 234127 5832 ns/op 3028 B/op 44 allocs/op +PASS +ok tygit.tuyoo.com/gocomponents/galog 3.190s + +# 100w行日志文件,写入性能未见明显衰减 +BenchmarkGaLog-8 89175 12481 ns/op 7590 B/op 90 allocs/op +BenchmarkAssetLog-8 195075 5933 ns/op 3028 B/op 44 allocs/op +``` + +- 异步写入模式(默认) +``` +goos: darwin +goarch: arm64 +pkg: tygit.tuyoo.com/gocomponents/galog +BenchmarkGaLog-8 95530 11331 ns/op 7599 B/op 90 allocs/op +BenchmarkAssetLog-8 337963 3512 ns/op 3032 B/op 44 allocs/op +PASS +ok tygit.tuyoo.com/gocomponents/galog 3.040s + +# 120w行日志文件,写入性能未见明显衰减 +BenchmarkGaLog-8 116559 10450 ns/op 7598 B/op 90 allocs/op +BenchmarkAssetLog-8 335670 3512 ns/op 3032 B/op 44 allocs/op +``` + +## Load Testing + +CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o galog examples/hey.go + +- 测试机:sa101-ecs-bj4-pt57-test-wxy +- linux服务器配置:2c4G +- 测试时间:2024-02-26 17:45 ~ 18:45 + +``` +./galog -n 1000000 -c 2 -q 100 +``` + +| 压测说明 | CPU平台使用率 | 内存使用率 | IO平均使用率 | 进程打开的文件句柄数 | +|----- | ----- | ----- | ----- | ----- | +| 无任务 | 1% | 52% | 0.08% | 0 | +| 并发=2,qps=200 | 1% | 52% | 0.08% | 7 | +| 并发=5,qps=5000 | 4% | 52% | 1% | 7 | +| 并发=5,qps=50000(队列1000,有阻塞) realqps=5000 | 40% | 52% | 30% | 7 | +| 并发=5,qps=50000(队列10000,有阻塞) realqps=6500 | 6% | 52% | 1% | 7 | +| 并发=20,qps=50000(队列10000,有阻塞) realqps=18000 | 10% | 52% | 3% | 7 | +| 并发=50,qps=50000(队列10000) realqps=50000 | 35% | 52% | 10% | 7 | +| 并发=100,qps=100000(队列10000) realqps=90000 | 75% | 52% | 16% | 7 | + +- 压测结果:写入队列=10000,单核支持最高写入QPS约5w,CPU=75%。 + +- 监控结果如下: +![img2.png](img/WX20240226-183250@2x.png) +![img2.png](img/WX20240226-184752@2x.png) + +## ga日志格式范式 + +```json +{ + "project_id": "20437", + "type": "track", + "event": "sdk_sword_holder_succ", + "event_time": 1708568434744, + "device_id": "", + "user_id": "908825698", + "client_id": "Android_5.1_tyGuest,weixinPay,tyAccount.alipay.0-hall20437.tuyoo.sdkonline", + "properties": { + "sdk_track_id": "3:6060:636978360:1708568434", + "sdk_sub_channel": "fish3d", + "country": "中国", + "proj_game_id": "", + "sdk_error_code": "", + "sdk_error_msg": "", + "city": "宁德市", + "sdk_sword_track_id": "807370ff-ceac-4ee3-b68f-50642ca4953c", + "sub_platform_id": "2", + "sdk_sword_holder_id": "320", + "sdk_sword_version": "v1.1.7", + "uuid": "56fdf0f9-9daa-4bf6-be69-88e15f05c36c", + "province": "福建省", + "sdk_login_channel_type": "", + "app_id": "20437", + "sdk_s_login_channel_type": "", + "sdk_main_channel": "official", + "game_id": "20437", + "sdk_s_route": "/api/sworder-server/rule/v1/getUserRisk", + "sdk_sword_holder_result": "999999", + "proj_cloud_id": "3", + "proj_app_id": "10010", + "ip_address": "59.58.58.18", + "sdk_sword_holder_type": "login", + "sdk_sword_holder_name": "非信任设备禁止登录", + "proj_client_id": "Android_5.505_tyGuest,tyAccount,yidunlogin.weixinPay,alipay,yinlian,jingdong,weixinShare.0-hall28.official.fish3d", + "sdk_sword_holder_process": "{\"330\":[\"[330-0]未命中\"]}", + "sdk_yidun_device_id": "UuCJztYTtxRETUVUAEaFoCQaw8H2qkWE", + "platform_id": "1", + "sub_channel_id": "sdkonline", + "proj_package_name": "", + "channel_id": "tuyoo" + }, + "lib": { + "lib_version": "v1.0.0", + "lib_type": "go" + }, +} +``` + +## asset日志格式范例 + +```json +{ + "project_id": "28", + "type": "asset", + "event": "asset_increase", + "event_time": 1706631881042, + "device_id": "", + "user_id": "928426614", + "client_id": "Android_4.827_tyGuest,nearme.nearme.0-hall28.oppo.bydzz", + "properties": { + "proj_ga_eventId": "STARTUP_QUEST_REWARD_COIN", + "proj_asset_value": "2", + "proj_asset_final": "2", + "proj_chip_type": "6", + "proj_asset_id": "13101", + "proj_asset_name": "\u9501\u5b9a", + "proj_asset_type": "free", + "proj_asset_time_limit": "0", + "proj_asset_start_time": "0", + "proj_asset_source": "", + "proj_tuyoo_order_id": "", + "game_id": "28", + "app_id": "10063", + "proj_project_id": "28", + "proj_client_id": "Android_4.827_tyGuest,nearme.nearme.0-hall28.oppo.bydzz", + "uuid": "77b907688c624076a84ca7b4b29668a5" + }, + "lib": { + "lib_version": "v1.0.0", + "lib_type": "go" + } +} +``` + +## Changelog + +| 版本 | 修订说明 | 提交人 | 发布时间 | +|----- | ----- | ----- | ----- | +| v1.0.0 | galog 0依赖、支持异步写入、小时切割日志、ga和asset日志类型 | 田文 | 2024.02.22 | +| v1.1.0 | add suger | 田文 | 2025.06.30 | \ No newline at end of file diff --git a/src/server/galog/buffer.go b/src/server/galog/buffer.go new file mode 100644 index 00000000..51b0e15d --- /dev/null +++ b/src/server/galog/buffer.go @@ -0,0 +1,78 @@ +package galog + +import ( + "sync" +) + +var ( + defaultBufPool = newBufPool() +) + +const ( + _size = 1024 + _defaultLineEnding = "\n" +) + +type BufPool struct { + p *sync.Pool +} + +func newBufPool() BufPool { + return BufPool{p: &sync.Pool{ + New: func() interface{} { + return &Buffer{bs: make([]byte, 0, _size)} + }, + }} +} + +// Get retrieves a Buffer from the pool, creating one if necessary. +func (p BufPool) Get() *Buffer { + buf := p.p.Get().(*Buffer) + buf.Reset() + buf.pool = p + return buf +} + +func (p BufPool) put(buf *Buffer) { + p.p.Put(buf) +} + +// Buffer is a thin wrapper around a byte slice. It's intended to be pooled, so +// the only way to construct one is via a Pool. +type Buffer struct { + bs []byte + pool BufPool +} + +// AppendByte writes a single byte to the Buffer. +func (b *Buffer) AppendByte(v byte) { + b.bs = append(b.bs, v) +} + +// AppendString writes a string to the Buffer. +func (b *Buffer) AppendString(s string) { + b.bs = append(b.bs, s...) +} + +// Bytes returns a mutable reference to the underlying byte slice. +func (b *Buffer) Bytes() []byte { + return b.bs +} + +// String returns a string copy of the underlying byte slice. +func (b *Buffer) String() string { + return string(b.bs) +} + +// Reset resets the underlying byte slice. Subsequent writes re-use the slice's +// backing array. +func (b *Buffer) Reset() { + b.bs = b.bs[:0] +} + +// Free returns the Buffer to its Pool. +// +// Callers must not retain references to the Buffer after calling Free. +func (b *Buffer) Free() { + b.pool.put(b) +} diff --git a/src/server/galog/entry.go b/src/server/galog/entry.go new file mode 100644 index 00000000..f0a43587 --- /dev/null +++ b/src/server/galog/entry.go @@ -0,0 +1,139 @@ +package galog + +var ( + defaultLib = map[string]string{"lib_type": "go", "lib_version": "v1.0.0"} +) + +type EntryBean struct { + // ProjectID 独立项目ID + ProjectID string `json:"project_id"` + // LogType 日志类型 默认是track + LogType LogType `json:"type"` + // Event 事件ID + Event string `json:"event"` + // EventTime 毫秒级时间 + EventTime int64 `json:"event_time"` + // DeviceID 设备ID + DeviceID string `json:"device_id"` + // UserID 用户ID + UserID string `json:"user_id"` + // ClientID clientID + ClientID string `json:"client_id"` + // Properties 重要额外字段 + Properties map[string]interface{} `json:"properties"` + // Lib 代码库 + Lib map[string]string `json:"lib"` +} + +type Entry struct { + logger *GALogger + bean *EntryBean +} + +func (e *Entry) SetUserID(uid string) *Entry { + if uid == "" { + uid = "0" + } + e.bean.UserID = uid + return e +} + +func (e *Entry) SetDeviceID(did string) *Entry { + e.bean.DeviceID = did + return e +} + +func (e *Entry) SetLib(lib map[string]string) *Entry { + e.bean.Lib = lib + return e +} + +func (e *Entry) SetClientID(cid string) *Entry { + e.bean.ClientID = cid + return e +} + +func (e *Entry) SetProperties(props map[string]interface{}) *Entry { + e.bean.Properties = props + return e +} + +// Flush 日志写入 +func (e *Entry) Flush() { + defer e.logger.putEntry(e) + e.logger.biz(e.bean) +} + +type AssetProperties map[string]interface{} + +// SetAssetID 资产id +func (ap AssetProperties) SetAssetID(assetID string) AssetProperties { + ap["proj_asset_id"] = assetID + return ap +} + +// SetAssetName 资产名称 +func (ap AssetProperties) SetAssetName(assetName string) AssetProperties { + ap["proj_asset_name"] = assetName + return ap +} + +// SetAssetType 资产付费类型 +func (ap AssetProperties) SetAssetType(assetType string) AssetProperties { + ap["proj_asset_type"] = assetType + return ap +} + +// SetAssetValue 资产变化数量 +func (ap AssetProperties) SetAssetValue(assetValue string) AssetProperties { + ap["proj_asset_value"] = assetValue + return ap +} + +// SetAssetFinal 资产变化后的剩余数量 +func (ap AssetProperties) SetAssetFinal(assetFinal string) AssetProperties { + ap["proj_asset_final"] = assetFinal + return ap +} + +// SetAssetTimeLimit 持续性资产持续时间 +func (ap AssetProperties) SetAssetTimeLimit(assetTimeLimit string) AssetProperties { + ap["proj_asset_time_limit"] = assetTimeLimit + return ap +} + +// SetAssetStartTime 持续性资产生效时间 +func (ap AssetProperties) SetAssetStartTime(assetStartTime string) AssetProperties { + ap["proj_asset_start_time"] = assetStartTime + return ap +} + +// SetAssetSource 变化的原因 +func (ap AssetProperties) SetAssetSource(assetSource string) AssetProperties { + ap["proj_asset_source"] = assetSource + return ap +} + +// SetAssetAssociated 变化关联资产 +func (ap AssetProperties) SetAssetAssociated(assetAssociated string) AssetProperties { + ap["proj_asset_associated"] = assetAssociated + return ap +} + +// SetTuyooOrderID SDK订单号 +func (ap AssetProperties) SetTuyooOrderID(tuyooOrderID string) AssetProperties { + ap["proj_tuyoo_order_id"] = tuyooOrderID + return ap +} + +// SetOrderID 游戏服订单号 +func (ap AssetProperties) SetOrderID(assetOrderID string) AssetProperties { + ap["proj_order_id"] = assetOrderID + return ap +} + +// SetKV 设置自定义字段必须通知到数据组,否则打点失败 +func (ap AssetProperties) SetKV(k, v string) AssetProperties { + ap[k] = v + return ap +} diff --git a/src/server/galog/examples/hey.go b/src/server/galog/examples/hey.go new file mode 100644 index 00000000..80c7ffe8 --- /dev/null +++ b/src/server/galog/examples/hey.go @@ -0,0 +1,83 @@ +package main + +import ( + "flag" + "fmt" + "os" + "os/signal" + "runtime" + + "tygit.tuyoo.com/gocomponents/galog/examples/worker" +) + +var ( + output = flag.String("o", "", "") + + c = flag.Int("c", 50, "") + n = flag.Int("n", 200, "") + q = flag.Float64("q", 0, "") + + cpus = flag.Int("cpus", runtime.GOMAXPROCS(-1), "") +) + +var usage = `Usage: hey [options...] + +Options: + -n Number of requests to run. Default is 200. + -c Number of workers to run concurrently. Total number of requests cannot + be smaller than the concurrency level. Default is 50. + -q Rate limit, in queries per second (QPS) per worker. Default is no rate limit. + -o Output type. If none provided, a summary is printed. + "csv" is the only supported alternative. Dumps the response + metrics in comma-separated values format. + + -cpus Number of used cpu cores. + (default for current machine is %d cores) +` + +func MainHey() { + flag.Usage = func() { + fmt.Fprintf(os.Stderr, usage, runtime.NumCPU()) + } + + flag.Parse() + + runtime.GOMAXPROCS(*cpus) + num := *n + conc := *c + q := *q + + if num <= 0 || conc <= 0 { + usageAndExit("-n and -c cannot be smaller than 1.") + } + + if num < conc { + usageAndExit("-n cannot be less than -c.") + } + + w := &worker.Work{ + N: num, + C: conc, + QPS: q, + Output: *output, + } + w.Init() + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + go func() { + <-c + w.Stop() + }() + w.Run() +} + +func usageAndExit(msg string) { + if msg != "" { + fmt.Fprint(os.Stderr, msg) + fmt.Fprintf(os.Stderr, "\n\n") + } + flag.Usage() + fmt.Fprintf(os.Stderr, "\n") + os.Exit(1) +} diff --git a/src/server/galog/examples/main.go b/src/server/galog/examples/main.go new file mode 100644 index 00000000..c8944256 --- /dev/null +++ b/src/server/galog/examples/main.go @@ -0,0 +1,107 @@ +package main + +import ( + "log" + "time" + + "tygit.tuyoo.com/gocomponents/galog" +) + +func main() { + log.Println("log ga asset server every 5 seconds.") + go logGA() + go logAsset() + logServer() +} + +func logGA() { + projectID := "20433" + clientID := "Android_5.0_tyGuest,weixinPay,tyAccount.alipay.0-hall20433.tuyoo.sdktest" + logger, err := galog.NewServerGALogger(&galog.ServerGALogOptions{ + ProjectID: projectID, + ClientID: clientID, + LogDir: "ga_log", + LogType: galog.LogTypeTrack, + EnableAsync: true, + AsyncQueueSize: 1000, + }) + if err != nil { + log.Fatal(err) + } + t := time.NewTicker(5 * time.Second) + + for tt := range t.C { + log.Println("logGA") + logger. + GetEntry("sdk_s_login_succ"). + SetDeviceID("device001"). + SetUserID("10086"). + SetProperties(map[string]interface{}{ + "ip_address": "127.0.0.1", + "proj_app_id": "10010", + "uuid": "4951d472-2c46-4fe5-9c4f-c35b6fb53f67", + "ts": tt.UnixNano(), + }). + Flush() + } +} + +func logAsset() { + projectID := "28" + clientID := "Android_4.827_tyGuest,nearme.nearme.0-hall28.oppo.bydzz" + logger, err := galog.NewGALogger(".", projectID, clientID, galog.LogTypeAsset) + if err != nil { + log.Fatal(err) + } + t := time.NewTicker(5 * time.Second) + + asset := galog.AssetProperties{} + asset.SetAssetID("13101"). + SetAssetName("\u9501\u5b9a"). + SetAssetType("6"). + SetAssetFinal("2"). + SetAssetAssociated("3"). + SetAssetStartTime("0"). + SetAssetTimeLimit("0"). + SetAssetSource(""). + SetKV("uuid", "uuid-v4") + + for tt := range t.C { + log.Println("logAssert") + _ = tt + logger. + GetEntry("asset_increase"). + SetDeviceID(""). + SetUserID("10086"). + SetProperties(asset). + Flush() + } +} + +func logServer() { + slogger, _ := galog.NewServerLogger(&galog.ServerLogOptions{ + LogDir: "logs", + EnableAsync: true, + AsyncQueueSize: 1000, + }) + + t := time.NewTicker(5 * time.Second) + + for tt := range t.C { + log.Println("logServer") + _ = tt + + msgMap := map[string]interface{}{ + "CreateTime": time.Now().UnixNano() / int64(time.Microsecond), + "Host": "host", + "AppId": "10010", + "UserId": "12345", + "Level": "Notice", + "Entry": "Login", + "Func": "HandleLogin", + "TraceMsg": "HandlerWSFriApplyList|HandlerWSFriApplyList", + "Params": "{\"isoCode\":\"CN\",\"P0\":[3,\"Total: 3, End: 0, \"],\"pf\":\"wx\",\"appVer\":\"1.0\",\"sdkVer\":\"1.0\",\"ip\":\"58.247.195.158\",\"clientId\":\"7abd64bf-3fb6-4fef-a25e-e6562b7fb857\",\"timeZone\":\"Asia/Shanghai\",\"loginMark\":\"\",\"st\":1472}", + } + slogger.BizErr(msgMap) + } +} diff --git a/src/server/galog/examples/worker/print.go b/src/server/galog/examples/worker/print.go new file mode 100644 index 00000000..9e3f0bd7 --- /dev/null +++ b/src/server/galog/examples/worker/print.go @@ -0,0 +1,43 @@ +package worker + +import ( + "fmt" + "text/template" +) + +func newTemplate(output string) *template.Template { + outputTmpl := output + switch outputTmpl { + case "": + outputTmpl = defaultTmpl + case "csv": + outputTmpl = csvTmpl + } + return template.Must(template.New("tmpl").Funcs(tmplFuncMap).Parse(outputTmpl)) +} + +var tmplFuncMap = template.FuncMap{ + "formatNumber": formatNumber, + "formatNumberInt": formatNumberInt, +} + +func formatNumber(duration float64) string { + return fmt.Sprintf("%4.4f", duration) +} + +func formatNumberInt(duration int) string { + return fmt.Sprintf("%d", duration) +} + +var ( + defaultTmpl = ` +Summary: + Total: {{ formatNumber .Total.Seconds }} secs + Requests/sec: {{ formatNumber .Rps }} + + TotalNumRes: {{ .NumRes }} + AvgTotal: {{ .AvgTotal }} +` + csvTmpl = `{{ $connLats := .ConnLats }}{{ $dnsLats := .DnsLats }}{{ $dnsLats := .DnsLats }}{{ $reqLats := .ReqLats }}{{ $delayLats := .DelayLats }}{{ $resLats := .ResLats }}{{ $statusCodeLats := .StatusCodes }}{{ $offsets := .Offsets}}response-time,DNS+dialup,DNS,Request-write,Response-delay,Response-read,status-code,offset{{ range $i, $v := .Lats }} +{{ formatNumber $v }},{{ formatNumber (index $connLats $i) }},{{ formatNumber (index $dnsLats $i) }},{{ formatNumber (index $reqLats $i) }},{{ formatNumber (index $delayLats $i) }},{{ formatNumber (index $resLats $i) }},{{ formatNumberInt (index $statusCodeLats $i) }},{{ formatNumber (index $offsets $i) }}{{ end }}` +) diff --git a/src/server/galog/examples/worker/report.go b/src/server/galog/examples/worker/report.go new file mode 100644 index 00000000..d644e2a9 --- /dev/null +++ b/src/server/galog/examples/worker/report.go @@ -0,0 +1,94 @@ +package worker + +import ( + "bytes" + "fmt" + "io" + "log" + "time" +) + +type report struct { + avgTotal float64 + rps float64 + + results chan *result + done chan bool + total time.Duration + + numRes int64 + output string + + w io.Writer +} + +func newReport(w io.Writer, results chan *result, output string, n int) *report { + return &report{ + output: output, + results: results, + done: make(chan bool, 1), + w: w, + } +} + +func runReporter(r *report) { + // Loop will continue until channel is closed + for res := range r.results { + r.numRes++ + r.avgTotal += res.duration.Seconds() + } + // Signal reporter is done. + r.done <- true +} + +func (r *report) finalize(total time.Duration) { + r.total = total + r.rps = float64(r.numRes) / r.total.Seconds() + r.print() +} + +func (r *report) print() { + buf := &bytes.Buffer{} + if err := newTemplate(r.output).Execute(buf, r.snapshot()); err != nil { + log.Println("error:", err.Error()) + return + } + r.printf(buf.String()) + + r.printf("\n") +} + +func (r *report) printf(s string, v ...interface{}) { + fmt.Fprintf(r.w, s, v...) +} + +func (r *report) snapshot() Report { + snapshot := Report{ + AvgTotal: r.avgTotal, + Rps: r.rps, + Total: r.total, + NumRes: r.numRes, + } + + return snapshot +} + +type Report struct { + AvgTotal float64 + Rps float64 + + Total time.Duration + + NumRes int64 +} + +type LatencyDistribution struct { + Percentage int + Latency float64 +} + +type Bucket struct { + Mark float64 + Count int + Frequency float64 +} diff --git a/src/server/galog/examples/worker/worker.go b/src/server/galog/examples/worker/worker.go new file mode 100644 index 00000000..e5e4ef17 --- /dev/null +++ b/src/server/galog/examples/worker/worker.go @@ -0,0 +1,176 @@ +package worker + +import ( + "fmt" + "io" + "os" + "strconv" + "sync" + "time" + + "tygit.tuyoo.com/gocomponents/galog" +) + +var startTime = time.Now() + +// now returns time.Duration using stdlib time +func now() time.Duration { return time.Since(startTime) } + +// Max size of the buffer of result channel. +const maxResult = 1000000 + +type result struct { + offset time.Duration + duration time.Duration +} + +type Work struct { + // N is the total number of requests to make. + N int + + // C is the concurrency level, the number of concurrent workers to run. + C int + + // Qps is the rate limit in queries per second. + QPS float64 + + // Output represents the output type. If "csv" is provided, the + // output will be dumped as a csv stream. + Output string + + // Writer is where results will be written. If nil, results are written to stdout. + Writer io.Writer + + initOnce sync.Once + results chan *result + stopCh chan struct{} + start time.Duration + + report *report +} + +func (b *Work) writer() io.Writer { + if b.Writer == nil { + return os.Stdout + } + return b.Writer +} + +// Init initializes internal data-structures +func (b *Work) Init() { + b.initOnce.Do(func() { + b.results = make(chan *result, min(b.C*1000, maxResult)) + b.stopCh = make(chan struct{}, b.C) + b.initLogger() + }) +} + +// Run makes all the requests, prints the summary. It blocks until +// all work is done. +func (b *Work) Run() { + b.Init() + b.start = now() + b.report = newReport(b.writer(), b.results, b.Output, b.N) + // Run the reporter first, it polls the result channel until it is closed. + go func() { + runReporter(b.report) + }() + b.runWorkers() + b.Finish() +} + +func (b *Work) Stop() { + // Send stop signal so that workers can stop gracefully. + for i := 0; i < b.C; i++ { + b.stopCh <- struct{}{} + } +} + +func (b *Work) Finish() { + close(b.results) + total := now() - b.start + // Wait until the reporter is done. + <-b.report.done + b.report.finalize(total) +} + +var logger *galog.GALogger + +func (b *Work) initLogger() { + projectID := "28" + clientID := "Android_4.827_tyGuest,nearme.nearme.0-hall28.oppo.bydzz" + logger, _ = galog.NewGALogger("logs", projectID, clientID, galog.LogTypeAsset) +} + +func (b *Work) doSth() { + s := now() + + // doSth here. + asset := galog.AssetProperties{} + asset.SetAssetID("13101"). + SetAssetName("\u9501\u5b9a"). + SetAssetType("6"). + SetAssetFinal("2"). + SetAssetAssociated("3"). + SetAssetStartTime("0"). + SetAssetTimeLimit("0"). + SetAssetSource(""). + SetKV("uuid", strconv.Itoa(int(time.Now().UnixNano()))) + logger. + GetEntry("asset_increase"). + SetDeviceID(""). + SetUserID("10086"). + SetProperties(asset). + Flush() + + t := now() + finish := t - s + b.results <- &result{ + offset: s, + duration: finish, + } +} + +func (b *Work) runWorker(n int) { + var throttle <-chan time.Time + if b.QPS > 0 { + t := time.NewTicker(time.Duration(1e6/(b.QPS)) * time.Microsecond) + throttle = t.C + } + + for i := 0; i < n; i++ { + // Check if application is stopped. Do not send into a closed channel. + select { + case <-b.stopCh: + return + default: + if b.QPS > 0 { + <-throttle + } + b.doSth() + } + } +} + +func (b *Work) runWorkers() { + var wg sync.WaitGroup + wg.Add(b.C) + + fmt.Printf("==RUN worker==> N:%d C:%d QPS:%v b.N / b.C:%v", b.N, b.C, b.QPS, b.N/b.C) + + // Ignore the case where b.N % b.C != 0. + for i := 0; i < b.C; i++ { + go func() { + b.runWorker(b.N / b.C) + wg.Done() + }() + } + wg.Wait() +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/src/server/galog/file.go b/src/server/galog/file.go new file mode 100644 index 00000000..f7412225 --- /dev/null +++ b/src/server/galog/file.go @@ -0,0 +1,264 @@ +package galog + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path" + "path/filepath" + "strconv" + "strings" + "sync" + "time" +) + +type fileLogWriter struct { + sync.RWMutex + + Rotate bool `json:"rotate"` + Hourly bool `json:"hourly"` + + // The opened file + Filename string `json:"filename"` + fileWriter *os.File + + // Rotate hourly + MaxHours int64 `json:"maxhours"` // 默认不删除日志 + hourlyOpenDate int + hourlyOpenTime time.Time + + // Permissions for log file + Perm string `json:"perm"` + // Permissions for directory if it is specified in FileName + DirPerm string `json:"dirperm"` + + RotatePerm string `json:"rotateperm"` + + fileNameOnly, suffix string // like "project.log", project is fileNameOnly and .log is suffix +} + +func newFileWriter() LogProvider { + w := &fileLogWriter{ + Rotate: true, // 开启日志轮转 + Hourly: true, // 开启日志小时轮转 + RotatePerm: "0440", + Perm: "0660", + DirPerm: "0770", + } + return w +} + +// Init file logger with json config. +// jsonConfig like: +// +// {"filename":"logs/glog.log"} +func (w *fileLogWriter) Init(config string) error { + err := json.Unmarshal([]byte(config), w) + if err != nil { + return err + } + if w.Filename == "" { + return errors.New("jsonconfig must have filename") + } + w.suffix = filepath.Ext(w.Filename) + w.fileNameOnly = strings.TrimSuffix(w.Filename, w.suffix) + if w.suffix == "" { + w.suffix = ".log" + } + + err = w.startLogger() + return err +} + +func (w *fileLogWriter) startLogger() error { + file, err := w.createLogFile() + if err != nil { + return err + } + if w.fileWriter != nil { + w.fileWriter.Close() + } + w.fileWriter = file + return w.initFd() +} + +func (w *fileLogWriter) needRotateHourly(hour int) bool { + return w.Hourly && hour != w.hourlyOpenDate +} + +func (w *fileLogWriter) createLogFile() (*os.File, error) { + perm, err := strconv.ParseInt(w.Perm, 8, 64) + if err != nil { + return nil, err + } + + dirperm, err := strconv.ParseInt(w.DirPerm, 8, 64) + if err != nil { + return nil, err + } + + filepath := path.Dir(w.Filename) + os.MkdirAll(filepath, os.FileMode(dirperm)) + + fd, err := os.OpenFile(w.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(perm)) + if err == nil { + os.Chmod(w.Filename, os.FileMode(perm)) + } + return fd, err +} + +func (w *fileLogWriter) initFd() error { + w.hourlyOpenTime = time.Now() + w.hourlyOpenDate = w.hourlyOpenTime.Hour() + if w.Hourly { + go w.hourlyRotate(w.hourlyOpenTime) + } + return nil +} + +func (w *fileLogWriter) hourlyRotate(openTime time.Time) { + y, m, d := openTime.Add(1 * time.Hour).Date() + h := openTime.Add(1 * time.Hour).Hour() + nextHour := time.Date(y, m, d, h, 0, 0, 0, openTime.Location()) + tm := time.NewTimer(time.Duration(nextHour.UnixNano() - openTime.UnixNano() + 100)) + <-tm.C + w.Lock() + if w.needRotateHourly(time.Now().Hour()) { + if err := w.doRotate(); err != nil { + fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err) + } + } + w.Unlock() +} + +func (w *fileLogWriter) doRotate() error { + fName := "" + format := "" + var openTime time.Time + rotatePerm, err := strconv.ParseInt(w.RotatePerm, 8, 64) + if err != nil { + return err + } + + _, err = os.Lstat(w.Filename) + if err != nil { + goto RESTART_LOGGER + } + + if w.Hourly { + format = "20060102_15" + openTime = w.hourlyOpenTime + } + + fName = w.fileNameOnly + fmt.Sprintf("_%s%s", openTime.Format(format), w.suffix) + _, err = os.Lstat(fName) + + // return error if the last file checked still existed + if err == nil { + return fmt.Errorf("rotate: cannot find free log number to rename %s", w.Filename) + } + + w.fileWriter.Close() + + // Rename the file to its new found name + // even if occurs error,we MUST guarantee to restart new logger + err = os.Rename(w.Filename, fName) + if err != nil { + goto RESTART_LOGGER + } + + err = os.Chmod(fName, os.FileMode(rotatePerm)) + +RESTART_LOGGER: + + startLoggerErr := w.startLogger() + if w.MaxHours > 0 { + go w.deleteOldLog() + } + + if startLoggerErr != nil { + return fmt.Errorf("rotate startLogger: %s", startLoggerErr) + } + if err != nil { + return fmt.Errorf("rotate: %s", err) + } + return nil +} + +func (w *fileLogWriter) deleteOldLog() { + dir := filepath.Dir(w.Filename) + absolutePath, err := filepath.EvalSymlinks(w.Filename) + if err == nil { + dir = filepath.Dir(absolutePath) + } + filepath.Walk(dir, func(path string, info os.FileInfo, err error) (returnErr error) { + defer func() { + if r := recover(); r != nil { + fmt.Fprintf(os.Stderr, "Unable to delete old log '%s', error: %v\n", path, r) + } + }() + + if info == nil { + return + } + if w.Hourly { + if !info.IsDir() && info.ModTime().Add(1*time.Hour*time.Duration(w.MaxHours)).Before(time.Now()) { + if strings.HasPrefix(filepath.Base(path), filepath.Base(w.fileNameOnly)) && + strings.HasSuffix(filepath.Base(path), w.suffix) { + os.Remove(path) + } + } + } + return + }) +} + +// EncodeMsg encode log msg +func (*fileLogWriter) EncodeMsg(lm *LogMsg) []byte { + buf := defaultBufPool.Get() + buf.AppendString(lm.Msg) + buf.AppendString(_defaultLineEnding) + msg := buf.Bytes() + buf.Free() + return msg +} + +// WriteMsg write msg to log and rotate +func (w *fileLogWriter) WriteMsg(lm *LogMsg) error { + h := lm.When.Hour() + + if w.Rotate { + w.RLock() + if w.needRotateHourly(h) { + w.RUnlock() + w.Lock() + if w.needRotateHourly(h) { + if err := w.doRotate(); err != nil { + fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err) + } + } + w.Unlock() + } else { + w.RUnlock() + } + } + + msg := w.EncodeMsg(lm) + _, err := w.fileWriter.Write(msg) + return err +} + +// Destroy close the file description, close file writer. +func (w *fileLogWriter) Destroy() { + w.fileWriter.Close() +} + +// Flush flushes file logger. +func (w *fileLogWriter) Flush() { + w.fileWriter.Sync() +} + +func init() { + Register(AdapterFile, newFileWriter) +} diff --git a/src/server/galog/galog.go b/src/server/galog/galog.go new file mode 100644 index 00000000..da1241c0 --- /dev/null +++ b/src/server/galog/galog.go @@ -0,0 +1,99 @@ +package galog + +import ( + "fmt" + "os" + "strings" + "sync" + "time" +) + +type GALogger struct { + logger *Logger + projectID string + clientID string + logType LogType + entryPool sync.Pool +} + +type LogType string + +const ( + // LogTypeTrack 事实日志 + LogTypeTrack LogType = "track" + // LogTypeAsset 资产日志 + LogTypeAsset LogType = "asset" +) + +const ( + // enableAsync 启用异步写入模式 + enableAsync = true + // defaultAsyncMsgLen 异步写入模式的队列长度,QPS较大时可调整此值 + defaultAsyncMsgLen int64 = 100 +) + +// NewGALogger +// +// logDir: 日志根目录 logs | /home/tywork +// projectID: projectID +// clientID: clientID +// logType: galog.LogTypeTrack | galog.LogTypeAsset +func NewGALogger(logDir, projectID, clientID string, logType LogType) (*GALogger, error) { + l := newLogger() + hostname, _ := os.Hostname() + filepath := fmt.Sprintf("ga_log/ga_%s.log", hostname) + if logType == LogTypeAsset { + filepath = fmt.Sprintf("asset_log/%s_%s.log", logType, hostname) + } + configs := fmt.Sprintf(`{"filename":"%s/%s"}`, strings.TrimRight(logDir, "/"), filepath) + err := l.setLogger(AdapterFile, configs) + if err != nil { + return nil, err + } + if enableAsync { + l.Async() + } + gaLogger := &GALogger{ + logger: l, + projectID: projectID, + clientID: clientID, + logType: logType, + } + + return gaLogger, nil +} + +// GetEntry +func (gl *GALogger) GetEntry(event string) *Entry { + e, ok := gl.entryPool.Get().(*Entry) + if ok { + e.logger = gl + e.bean.ProjectID = gl.projectID + e.bean.LogType = gl.logType + e.bean.ClientID = gl.clientID + e.bean.Event = event + e.bean.EventTime = time.Now().UnixMilli() + e.bean.Lib = defaultLib + return e + } + return &Entry{ + logger: gl, + bean: &EntryBean{ + ProjectID: gl.projectID, + LogType: gl.logType, + ClientID: gl.clientID, + Event: event, + EventTime: time.Now().UnixMilli(), + Lib: defaultLib, + }, + } +} + +func (gl *GALogger) biz(v interface{}) { + gl.logger.BizErr(v) +} + +func (gl *GALogger) putEntry(entry *Entry) { + entry.bean = &EntryBean{} + gl.entryPool.Put(entry) +} diff --git a/src/server/galog/galog_test.go b/src/server/galog/galog_test.go new file mode 100644 index 00000000..805cc5c4 --- /dev/null +++ b/src/server/galog/galog_test.go @@ -0,0 +1,85 @@ +package galog + +import ( + "testing" + "time" +) + +var ( + gaLogger *GALogger + assetLogger *GALogger +) + +// go test -bench . -benchmem +func init() { + gaLogger, _ = NewGALogger("logs", "20433", "Android_5.0_tyGuest,weixinPay,tyAccount.alipay.0-hall20433.tuyoo.sdktest", LogTypeTrack) + assetLogger, _ = NewGALogger("logs", "28", "Android_4.827_tyGuest,nearme.nearme.0-hall28.oppo.bydzz", LogTypeAsset) +} + +func BenchmarkGaLog(b *testing.B) { + for i := 0; i < b.N; i++ { + props := map[string]interface{}{ + "ip_address": "127.0.0.1", + "proj_app_id": "10010", + "uuid": "4951d472-2c46-4fe5-9c4f-c35b6fb53f67", + "ts": time.Now().UnixNano(), + "ip_address1": "127.0.0.1", + "proj_app_id1": "10010", + "uuid1": "4951d472-2c46-4fe5-9c4f-c35b6fb53f67", + "ts1": time.Now().UnixNano(), + "ip_address2": "127.0.0.1", + "proj_app_id2": "10010", + "uuid2": "4951d472-2c46-4fe5-9c4f-c35b6fb53f67", + "ts2": time.Now().UnixNano(), + "ip_address3": "127.0.0.1", + "proj_app_id3": "10010", + "uuid3": "4951d472-2c46-4fe5-9c4f-c35b6fb53f67", + "ts4": time.Now().UnixNano(), + "proj_app_id25": "10010", + "uuid21": "4951d472-2c46-4fe5-9c4f-c35b6fb53f67", + "ts21": time.Now().UnixNano(), + "ip_address31": "127.0.0.1", + "proj_app_id31": "10010", + "uuid31": "4951d472-2c46-4fe5-9c4f-c35b6fb53f67", + "ts11": time.Now().UnixNano(), + "uuid3111": "4951d472-2c46-4fe5-9c4f-c35b6fb53f67", + "ts4111": time.Now().UnixNano(), + "proj_app_id21115": "10010", + "uuid111": "4951d472-2c46-4fe5-9c4f-c35b6fb53f67", + "ts111": time.Now().UnixNano(), + "ip_a11ddress31": "127.0.0.1", + "proj1_app_id31": "10010", + "uuid131": "4951d472-2c46-4fe5-9c4f-c35b6fb53f67", + "ts1111": time.Now().UnixNano(), + } + gaLogger. + GetEntry("sdk_s_login_succ"). + SetDeviceID("device001"). + SetUserID("10086"). + SetProperties(props). + Flush() + } +} + +func BenchmarkAssetLog(b *testing.B) { + for i := 0; i < b.N; i++ { + asset := make(AssetProperties) + asset. + SetAssetID("13101"). + SetAssetName("\u9501\u5b9a"). + SetAssetType("6"). + SetAssetFinal("2"). + SetAssetAssociated("3"). + SetAssetStartTime("0"). + SetAssetTimeLimit("0"). + SetAssetSource(""). + SetKV("uuid", "uuid-v4") + + assetLogger. + GetEntry("asset_increase"). + SetDeviceID(""). + SetUserID("10086"). + SetProperties(asset). + Flush() + } +} diff --git a/src/server/galog/go.mod b/src/server/galog/go.mod new file mode 100644 index 00000000..f044e2b2 --- /dev/null +++ b/src/server/galog/go.mod @@ -0,0 +1,3 @@ +module tygit.tuyoo.com/gocomponents/galog + +go 1.20 diff --git a/src/server/galog/img/WX20240226-183250@2x.png b/src/server/galog/img/WX20240226-183250@2x.png new file mode 100644 index 00000000..8bab3f3c Binary files /dev/null and b/src/server/galog/img/WX20240226-183250@2x.png differ diff --git a/src/server/galog/img/WX20240226-184752@2x.png b/src/server/galog/img/WX20240226-184752@2x.png new file mode 100644 index 00000000..318933f6 Binary files /dev/null and b/src/server/galog/img/WX20240226-184752@2x.png differ diff --git a/src/server/galog/log.go b/src/server/galog/log.go new file mode 100644 index 00000000..4294bfb7 --- /dev/null +++ b/src/server/galog/log.go @@ -0,0 +1,261 @@ +package galog + +import ( + "encoding/json" + "fmt" + "os" + "sync" + "time" + "unsafe" +) + +const ( + AdapterFile = "file" +) + +type LogMsg struct { + Msg string + When time.Time +} + +// Logger defines the behavior of a log provider. +type LogProvider interface { + Init(config string) error + WriteMsg(lm *LogMsg) error + Destroy() + Flush() +} + +type newLogProviderFunc func() LogProvider + +var ( + adapters = make(map[string]newLogProviderFunc) +) + +// Register register a new log provider. +func Register(name string, log newLogProviderFunc) { + if log == nil { + panic("logs: Register provide is nil") + } + if _, dup := adapters[name]; dup { + panic("logs: Register called twice for provider " + name) + } + adapters[name] = log +} + +type Logger struct { + lock sync.Mutex + asynchronous bool + wg sync.WaitGroup + msgChanLen int64 + msgChan chan *LogMsg + closeChan chan struct{} + flushChan chan struct{} + outputs []*nameLogger +} + +type nameLogger struct { + LogProvider + name string +} + +var logMsgPool *sync.Pool + +// newLogger return a new Logger. +func newLogger(channelLens ...int64) *Logger { + gl := new(Logger) + gl.msgChanLen = append(channelLens, 0)[0] + if gl.msgChanLen <= 0 { + gl.msgChanLen = defaultAsyncMsgLen + } + gl.flushChan = make(chan struct{}, 1) + gl.closeChan = make(chan struct{}, 1) + return gl +} + +// Async sets the log to asynchronous and start the goroutine +func (gl *Logger) Async(msgLen ...int64) *Logger { + gl.lock.Lock() + defer gl.lock.Unlock() + if gl.asynchronous { + return gl + } + gl.asynchronous = true + if len(msgLen) > 0 && msgLen[0] > 0 { + gl.msgChanLen = msgLen[0] + } + gl.msgChan = make(chan *LogMsg, gl.msgChanLen) + logMsgPool = &sync.Pool{ + New: func() interface{} { + return &LogMsg{} + }, + } + gl.wg.Add(1) + go gl.startLogger() + return gl +} + +// SetLogger provides a given logger adapter into Logger with config string. +// config must in in JSON format like {"interval":360}} +func (gl *Logger) setLogger(adapterName string, configs ...string) error { + config := append(configs, "{}")[0] + + logAdapter, ok := adapters[adapterName] + if !ok { + return fmt.Errorf("logs: unknown adaptername %q (forgotten Register?)", adapterName) + } + + lg := logAdapter() + + err := lg.Init(config) + if err != nil { + return err + } + + gl.outputs = append(gl.outputs, &nameLogger{name: adapterName, LogProvider: lg}) + return nil +} + +func (gl *Logger) writeToLoggers(lm *LogMsg) { + for _, l := range gl.outputs { + err := l.WriteMsg(lm) + if err != nil { + fmt.Fprintf(os.Stderr, "unable to WriteMsg to adapter:%v,error:%v\n", l.name, err) + } + } +} + +// Write implements io.Writer. +func (gl *Logger) Write(p []byte) (n int, err error) { + if len(p) == 0 { + return 0, nil + } + // writeMsg will always add a '\n' character + if p[len(p)-1] == '\n' { + p = p[0 : len(p)-1] + } + lm := &LogMsg{ + Msg: string(p), + When: time.Now(), + } + + // set levelLoggerImpl to ensure all log message will be write out + err = gl.writeMsg(lm) + if err == nil { + return len(p), nil + } + return 0, err +} + +func (gl *Logger) writeMsg(lm *LogMsg) error { + if gl.asynchronous { + logM := logMsgPool.Get().(*LogMsg) + logM.Msg = lm.Msg + logM.When = lm.When + + if gl.outputs != nil { + gl.msgChan <- lm + } else { + logMsgPool.Put(lm) + } + } else { + gl.writeToLoggers(lm) + } + return nil +} + +func (gl *Logger) startLogger() { + gameOver := false + for { + select { + case bm, ok := <-gl.msgChan: + if ok { + gl.writeToLoggers(bm) + logMsgPool.Put(bm) + } + case <-gl.closeChan: + gl.flush() + for _, l := range gl.outputs { + l.Destroy() + } + gl.outputs = nil + gameOver = true + gl.wg.Done() + case <-gl.flushChan: + gl.flush() + gl.wg.Done() + } + if gameOver { + break + } + } +} + +// Info Log INFO level message. +func (gl *Logger) Info(format string) { + lm := &LogMsg{ + Msg: format, + When: time.Now(), + } + + gl.writeMsg(lm) +} + +// BizErr Log a json-interface +func (gl *Logger) BizErr(v interface{}) error { + buf, err := json.Marshal(v) + if err != nil { + return err + } + str := *(*string)(unsafe.Pointer(&buf)) + gl.Info(str) + return nil +} + +// Flush flush all chan data. +func (gl *Logger) Flush() { + if gl.asynchronous { + gl.flushChan <- struct{}{} + gl.wg.Wait() + gl.wg.Add(1) + return + } + gl.flush() +} + +// Close close logger, flush all chan data and destroy all adapters in Logger. +func (gl *Logger) Close() { + if gl.asynchronous { + gl.closeChan <- struct{}{} + gl.wg.Wait() + close(gl.msgChan) + } else { + gl.flush() + for _, l := range gl.outputs { + l.Destroy() + } + gl.outputs = nil + } + close(gl.flushChan) + close(gl.closeChan) +} + +func (gl *Logger) flush() { + if gl.asynchronous { + for { + if len(gl.msgChan) > 0 { + bm, ok := <-gl.msgChan + if !ok { + continue + } + gl.writeToLoggers(bm) + logMsgPool.Put(bm) + continue + } + break + } + } + for _, l := range gl.outputs { + l.Flush() + } +} diff --git a/src/server/galog/suger.go b/src/server/galog/suger.go new file mode 100644 index 00000000..6d4a0bc7 --- /dev/null +++ b/src/server/galog/suger.go @@ -0,0 +1,122 @@ +package galog + +import ( + "errors" + "fmt" + "os" + "strings" +) + +type ServerLogOptions struct { + // LogDir 日志目录,默认logs + LogDir string + + // EnableAsync 是否开启异步写日志,默认开启 + EnableAsync bool + + // AsyncQueueSize 异步队列大小,默认1000 + AsyncQueueSize int64 +} + +// NewServerLogger 业务日志实例,support JSON +// +// opts.LogDir 日志目录,推荐logs +// opts.EnableAsync 是否开启异步写日志,默认开启 +// opts.AsyncQueueSize 异步队列大小,默认1000 +// useage: logger.BizErr(map[string]interface{}{"type": "test"}) +func NewServerLogger(opts *ServerLogOptions) (*Logger, error) { + if opts == nil { + opts = &ServerLogOptions{ + LogDir: "logs", + EnableAsync: true, + AsyncQueueSize: 1000, + } + } + if opts.LogDir == "" { + opts.LogDir = "logs" + } + if opts.EnableAsync && opts.AsyncQueueSize == 0 { + opts.AsyncQueueSize = 1000 + } + + l := newLogger() + hostname, _ := os.Hostname() + filename := fmt.Sprintf("server_%s.log", hostname) + configs := fmt.Sprintf(`{"filename":"%s/%s"}`, strings.TrimRight(opts.LogDir, "/"), filename) + err := l.setLogger(AdapterFile, configs) + if err != nil { + return nil, err + } + if opts.EnableAsync && opts.AsyncQueueSize > 0 { + l.Async(opts.AsyncQueueSize) + } + return l, nil +} + +type ServerGALogOptions struct { + // LogDir 日志目录,默认galogs + LogDir string + + // EnableAsync 是否开启异步写日志,默认开启 + EnableAsync bool + + // AsyncQueueSize 异步队列大小,默认1000 + AsyncQueueSize int64 + + // GA侧分配ProjectID + ProjectID string + + // GA侧分配ClientID + ClientID string + + LogType LogType +} + +// NewServerGALogger GA日志实例,support JSON +// +// opts.ProjectID 项目ID,GA侧分配,必填 +// opts.ClientID 客户端ID,GA侧分配,必填 +// opts.LogType 日志类型,默认LogTypeTrack +// opts.LogDir 日志目录,默认galogs +// opts.EnableAsync 是否开启异步写日志,默认开启 +// opts.AsyncQueueSize 异步队列大小,默认1000 +func NewServerGALogger(opts *ServerGALogOptions) (*GALogger, error) { + if opts == nil { + return nil, errors.New("opts must not be empty") + } + if opts.EnableAsync && opts.AsyncQueueSize == 0 { + opts.AsyncQueueSize = 1000 + } + if opts.LogDir == "" { + opts.LogDir = "galogs" + } + if opts.LogType == "" { + opts.LogType = LogTypeTrack + } + if opts.ProjectID == "" || opts.ClientID == "" { + return nil, errors.New("projectID or clientID must not be empty") + } + + l := newLogger() + hostname, _ := os.Hostname() + filename := fmt.Sprintf("ga_%s.log", hostname) + if opts.LogType == LogTypeAsset { + filename = fmt.Sprintf("asset_%s.log", hostname) + } + configs := fmt.Sprintf(`{"filename":"%s/%s"}`, strings.TrimRight(opts.LogDir, "/"), filename) + err := l.setLogger(AdapterFile, configs) + if err != nil { + return nil, err + } + if opts.EnableAsync && opts.AsyncQueueSize > 0 { + l.Async(opts.AsyncQueueSize) + } + gaLogger := &GALogger{ + logger: l, + projectID: opts.ProjectID, + clientID: opts.ClientID, + logType: opts.LogType, + } + + return gaLogger, nil +} diff --git a/src/server/galog/suger_test.go b/src/server/galog/suger_test.go new file mode 100644 index 00000000..7ee2ebb8 --- /dev/null +++ b/src/server/galog/suger_test.go @@ -0,0 +1,34 @@ +package galog + +import ( + "testing" + "time" +) + +var slogger *Logger + +// go test -bench . -benchmem +func init() { + slogger, _ = NewServerLogger(&ServerLogOptions{ + LogDir: "logs", + EnableAsync: true, + }) +} + +// BenchmarkServerLog-8 436682 3103 ns/op 1838 B/op 24 allocs/op +func BenchmarkServerLog(b *testing.B) { + for i := 0; i < b.N; i++ { + msgMap := map[string]interface{}{ + "CreateTime": time.Now().UnixNano() / int64(time.Microsecond), + "Host": "host", + "AppId": "10010", + "UserId": "12345", + "Level": "Notice", + "Entry": "Login", + "Func": "HandleLogin", + "TraceMsg": "HandlerWSFriApplyList|HandlerWSFriApplyList", + "Params": "{\"isoCode\":\"CN\",\"P0\":[3,\"Total: 3, End: 0, \"],\"pf\":\"wx\",\"appVer\":\"1.0\",\"sdkVer\":\"1.0\",\"ip\":\"58.247.195.158\",\"clientId\":\"7abd64bf-3fb6-4fef-a25e-e6562b7fb857\",\"timeZone\":\"Asia/Shanghai\",\"loginMark\":\"\",\"st\":1472}", + } + slogger.BizErr(msgMap) + } +} diff --git a/src/server/game/GameLogic.go b/src/server/game/GameLogic.go index 1fc2ff4e..373faf68 100644 --- a/src/server/game/GameLogic.go +++ b/src/server/game/GameLogic.go @@ -988,19 +988,6 @@ func NotifyPlayer(Uid int, m *MsgMod.Msg) { p.Send(m) } -func setRedisLock(key string, Duration time.Duration) bool { - return db.RedisLock(key, "lock", Duration) -} - -func getRedisLock(key string) error { - _, err := db.RedisGetKey(key) - return err -} - -func unsetRedisLock(key string) { - db.RedisUnlock(key, "") -} - func Destroy() { log.Debug("服务器下线") if G_GameLogicPtr != nil { diff --git a/src/server/game/Player.go b/src/server/game/Player.go index 8d7a89c3..d11a824a 100644 --- a/src/server/game/Player.go +++ b/src/server/game/Player.go @@ -1,12 +1,10 @@ package game import ( - // "server/GoUtil" - // "server/MergeConst" - + "bytes" "context" "database/sql" - "encoding/json" + "encoding/gob" "errors" "math" "server/GoUtil" @@ -20,6 +18,7 @@ import ( miningCfg "server/conf/mining" playroomCfg "server/conf/playroom" "server/db" + "server/ga" "server/game/mod/activity" "server/game/mod/friend" "server/game/mod/item" @@ -980,9 +979,17 @@ func (p *Player) UpdateUserInfo() { simple.CardInfo = CardMod.GetCardList() simple.ActLog = p.PlayMod.getFriendMod().GetActLogLast() simple.Physiology = p.PlayMod.getPlayroomMod().GetPhysiologyList() - value, _ := json.Marshal(simple) + + // 使用 gob 编码替代 json,生成更紧凑的二进制 + var buf bytes.Buffer + enc := gob.NewEncoder(&buf) + if err := enc.Encode(simple); err != nil { + log.Debug("gob encode simple failed: %v", err) + return + } + value := buf.Bytes() IdStr := strconv.Itoa(int(p.M_DwUin)) - go db.RedisSetKey(IdStr, string(value), 0) + go db.RedisSetKeyBytes(IdStr, value, 0) } func (p *Player) HandleInUserRank() { @@ -1050,6 +1057,10 @@ func (p *Player) TeLog(Type string, Param map[string]interface{}) { Param["Ip"] = agent.RemoteAddr().String() } telog.Te.Track(p.GetPlayerBaseMod().GetName(), p.GetPlayerBaseMod().GetName(), Type, Param) + BaseMod := p.PlayMod.getBaseMod() + + //途游GA + ga.GAlogEvent(Type, BaseMod.Account, "", Param) } func (p *Player) Kafka(Type string, Param map[string]interface{}) { diff --git a/src/server/game/external.go b/src/server/game/external.go index bd3dd209..23305e4d 100644 --- a/src/server/game/external.go +++ b/src/server/game/external.go @@ -184,6 +184,7 @@ func HandleClientReq(args []interface{}) { db.UpdateAccountInfoDeviceToDb(accountInfo) p, _ := internal.Agents.Load(a) if p != nil { + p.(*Player).PlayMod.getBaseMod().DiviceId = detail.Device //加锁 p.(*Player).PushClientRes(ResLogin) p.(*Player).LoginBackData() G_GameLogicPtr.AddLog(&Log{ diff --git a/src/server/game/mod/base/Base.go b/src/server/game/mod/base/Base.go index a060a802..9c155784 100644 --- a/src/server/game/mod/base/Base.go +++ b/src/server/game/mod/base/Base.go @@ -43,6 +43,7 @@ type Base struct { IdCardName string IdCardNum string AddCode string // 用于添加好友的code + DiviceId string // 设备id } func (b *Base) InitData(Uid int, Ip string) { diff --git a/src/server/go.mod b/src/server/go.mod index 0bc9f684..b5f48239 100644 --- a/src/server/go.mod +++ b/src/server/go.mod @@ -18,7 +18,6 @@ require ( github.com/robfig/cron/v3 v3.0.1 github.com/segmentio/kafka-go v0.4.47 github.com/shirou/gopsutil v3.21.11+incompatible - github.com/vicanso/go-charts/v2 v2.6.10 google.golang.org/protobuf v1.36.2 gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 ) @@ -35,9 +34,7 @@ require ( github.com/alibabacloud-go/tea-utils v1.4.5 // indirect github.com/alibabacloud-go/tea-xml v1.1.3 // indirect github.com/clbanning/mxj/v2 v2.7.0 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect github.com/go-ole/go-ole v1.2.6 // indirect - github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.15.9 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -47,9 +44,7 @@ require ( github.com/tjfoc/gmsm v1.4.1 // indirect github.com/tklauser/go-sysconf v0.3.14 // indirect github.com/tklauser/numcpus v0.8.0 // indirect - github.com/wcharczuk/go-chart/v2 v2.1.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 // indirect golang.org/x/net v0.34.0 // indirect golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect @@ -68,4 +63,7 @@ require ( github.com/google/uuid v1.6.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + github.com/tuyou/galog v0.0.0 ) + +replace github.com/tuyou/galog => ./galog diff --git a/src/server/go.sum b/src/server/go.sum index 886ebc10..8db7fcaf 100644 --- a/src/server/go.sum +++ b/src/server/go.sum @@ -92,8 +92,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= @@ -103,8 +101,6 @@ github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -189,10 +185,6 @@ github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZ github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= -github.com/vicanso/go-charts/v2 v2.6.10 h1:Nb2YBekEbUBPbvohnUO1oYMy31v75brUPk6n/fq+JXw= -github.com/vicanso/go-charts/v2 v2.6.10/go.mod h1:Ii2KDI3udTG1wPtiTnntzjlUBJVJTqNscMzh3oYHzUk= -github.com/wcharczuk/go-chart/v2 v2.1.0 h1:tY2slqVQ6bN+yHSnDYwZebLQFkphK4WNrVwnt7CJZ2I= -github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= @@ -219,8 +211,6 @@ golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOM golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM= -golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=