日志优化
This commit is contained in:
parent
54bf168f80
commit
f435b444d7
@ -27,6 +27,7 @@ import (
|
|||||||
"server/game/mod/quest"
|
"server/game/mod/quest"
|
||||||
GoUtil "server/game_util"
|
GoUtil "server/game_util"
|
||||||
"server/msg"
|
"server/msg"
|
||||||
|
telog "server/thinkdata"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@ -1061,7 +1062,7 @@ func (p *Player) TeLog(Type string, Param map[string]interface{}) {
|
|||||||
}
|
}
|
||||||
//Param["#zone_offset"] = -5
|
//Param["#zone_offset"] = -5
|
||||||
// 游戏内TE日志
|
// 游戏内TE日志
|
||||||
//go telog.Te.Track(p.GetPlayerBaseMod().GetName(), p.GetPlayerBaseMod().GetName(), Type, Param)
|
go telog.Te.Track(p.GetPlayerBaseMod().GetName(), p.GetPlayerBaseMod().GetName(), Type, Param)
|
||||||
BaseMod := p.PlayMod.getBaseMod()
|
BaseMod := p.PlayMod.getBaseMod()
|
||||||
//途游GA
|
//途游GA
|
||||||
go ga.GAlogEvent(Type, BaseMod.Account, "", Param)
|
go ga.GAlogEvent(Type, BaseMod.Account, "", Param)
|
||||||
|
|||||||
@ -2,8 +2,7 @@ package telog
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"server/conf"
|
"server/conf"
|
||||||
|
"server/thinkingdata"
|
||||||
"github.com/ThinkingDataAnalytics/go-sdk/v2/src/thinkingdata"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var Te thinkingdata.TDAnalytics
|
var Te thinkingdata.TDAnalytics
|
||||||
|
|||||||
352
src/server/thinkingdata/consumer_batch.go
Normal file
352
src/server/thinkingdata/consumer_batch.go
Normal file
@ -0,0 +1,352 @@
|
|||||||
|
package thinkingdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TDBatchConsumer upload data to TE by http
|
||||||
|
type TDBatchConsumer struct {
|
||||||
|
serverUrl string // serverUrl
|
||||||
|
appId string // appId
|
||||||
|
timeout time.Duration // http timeout (mill second)
|
||||||
|
compress bool // is need compress
|
||||||
|
bufferMutex *sync.RWMutex
|
||||||
|
cacheMutex *sync.RWMutex // cache mutex
|
||||||
|
|
||||||
|
buffer []Data
|
||||||
|
batchSize int // flush event count each time
|
||||||
|
cacheBuffer [][]Data // buffer
|
||||||
|
cacheCapacity int // buffer max count
|
||||||
|
}
|
||||||
|
|
||||||
|
type TDBatchConfig struct {
|
||||||
|
ServerUrl string // serverUrl
|
||||||
|
AppId string // appId
|
||||||
|
BatchSize int // flush event count each time
|
||||||
|
Timeout int // http timeout (mill second)
|
||||||
|
Compress bool // enable compress data
|
||||||
|
AutoFlush bool // enable auto flush
|
||||||
|
Interval int // auto flush spacing (second)
|
||||||
|
CacheCapacity int // cache event count
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultTimeOut = 30000
|
||||||
|
DefaultBatchSize = 20
|
||||||
|
MaxBatchSize = 200
|
||||||
|
DefaultInterval = 30
|
||||||
|
DefaultCacheCapacity = 50
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewBatchConsumer create TDBatchConsumer
|
||||||
|
func NewBatchConsumer(serverUrl string, appId string) (TDConsumer, error) {
|
||||||
|
config := TDBatchConfig{
|
||||||
|
ServerUrl: serverUrl,
|
||||||
|
AppId: appId,
|
||||||
|
Compress: true,
|
||||||
|
}
|
||||||
|
return initBatchConsumer(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBatchConsumerWithBatchSize create TDBatchConsumer
|
||||||
|
// serverUrl
|
||||||
|
// appId
|
||||||
|
// batchSize: flush event count each time
|
||||||
|
func NewBatchConsumerWithBatchSize(serverUrl string, appId string, batchSize int) (TDConsumer, error) {
|
||||||
|
config := TDBatchConfig{
|
||||||
|
ServerUrl: serverUrl,
|
||||||
|
AppId: appId,
|
||||||
|
Compress: true,
|
||||||
|
BatchSize: batchSize,
|
||||||
|
}
|
||||||
|
return initBatchConsumer(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBatchConsumerWithCompress create TDBatchConsumer
|
||||||
|
// serverUrl
|
||||||
|
// appId
|
||||||
|
// compress: enable data compress
|
||||||
|
func NewBatchConsumerWithCompress(serverUrl string, appId string, compress bool) (TDConsumer, error) {
|
||||||
|
config := TDBatchConfig{
|
||||||
|
ServerUrl: serverUrl,
|
||||||
|
AppId: appId,
|
||||||
|
Compress: compress,
|
||||||
|
}
|
||||||
|
return initBatchConsumer(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBatchConsumerWithConfig(config TDBatchConfig) (TDConsumer, error) {
|
||||||
|
return initBatchConsumer(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func initBatchConsumer(config TDBatchConfig) (TDConsumer, error) {
|
||||||
|
if config.ServerUrl == "" {
|
||||||
|
msg := fmt.Sprint("ServerUrl not be empty")
|
||||||
|
tdLogInfo(msg)
|
||||||
|
return nil, errors.New(msg)
|
||||||
|
}
|
||||||
|
u, err := url.Parse(config.ServerUrl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
u.Path = "/sync_server"
|
||||||
|
|
||||||
|
var batchSize int
|
||||||
|
if config.BatchSize > MaxBatchSize {
|
||||||
|
batchSize = MaxBatchSize
|
||||||
|
} else if config.BatchSize <= 0 {
|
||||||
|
batchSize = DefaultBatchSize
|
||||||
|
} else {
|
||||||
|
batchSize = config.BatchSize
|
||||||
|
}
|
||||||
|
|
||||||
|
var cacheCapacity int
|
||||||
|
if config.CacheCapacity <= 0 {
|
||||||
|
cacheCapacity = DefaultCacheCapacity
|
||||||
|
} else {
|
||||||
|
cacheCapacity = config.CacheCapacity
|
||||||
|
}
|
||||||
|
|
||||||
|
var timeout int
|
||||||
|
if config.Timeout == 0 {
|
||||||
|
timeout = DefaultTimeOut
|
||||||
|
} else {
|
||||||
|
timeout = config.Timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &TDBatchConsumer{
|
||||||
|
serverUrl: u.String(),
|
||||||
|
appId: config.AppId,
|
||||||
|
timeout: time.Duration(timeout) * time.Millisecond,
|
||||||
|
compress: config.Compress,
|
||||||
|
bufferMutex: new(sync.RWMutex),
|
||||||
|
cacheMutex: new(sync.RWMutex),
|
||||||
|
batchSize: batchSize,
|
||||||
|
buffer: make([]Data, 0, batchSize),
|
||||||
|
cacheCapacity: cacheCapacity,
|
||||||
|
cacheBuffer: make([][]Data, 0, cacheCapacity),
|
||||||
|
}
|
||||||
|
|
||||||
|
var interval int
|
||||||
|
if config.Interval == 0 {
|
||||||
|
interval = DefaultInterval
|
||||||
|
} else {
|
||||||
|
interval = config.Interval
|
||||||
|
}
|
||||||
|
if config.AutoFlush {
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(time.Duration(interval) * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
<-ticker.C
|
||||||
|
_ = c.timerFlush()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
tdLogInfo("Mode: batch consumer, appId: %s, serverUrl: %s", c.appId, c.serverUrl)
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TDBatchConsumer) Add(d Data) error {
|
||||||
|
c.bufferMutex.Lock()
|
||||||
|
c.buffer = append(c.buffer, d)
|
||||||
|
c.bufferMutex.Unlock()
|
||||||
|
|
||||||
|
tdLogInfo("Enqueue event data: %v", d)
|
||||||
|
|
||||||
|
if c.getBufferLength() >= c.batchSize || c.getCacheLength() > 0 {
|
||||||
|
err := c.Flush()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TDBatchConsumer) timerFlush() error {
|
||||||
|
tdLogInfo("timer flush data")
|
||||||
|
return c.innerFlush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TDBatchConsumer) Flush() error {
|
||||||
|
tdLogInfo("flush data")
|
||||||
|
return c.innerFlush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TDBatchConsumer) innerFlush() error {
|
||||||
|
|
||||||
|
c.cacheMutex.Lock()
|
||||||
|
defer c.cacheMutex.Unlock()
|
||||||
|
|
||||||
|
c.bufferMutex.Lock()
|
||||||
|
defer c.bufferMutex.Unlock()
|
||||||
|
|
||||||
|
if len(c.buffer) == 0 && len(c.cacheBuffer) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if len(c.cacheBuffer) > c.cacheCapacity {
|
||||||
|
c.cacheBuffer = c.cacheBuffer[1:]
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if len(c.cacheBuffer) == 0 || len(c.buffer) >= c.batchSize {
|
||||||
|
c.cacheBuffer = append(c.cacheBuffer, c.buffer)
|
||||||
|
c.buffer = make([]Data, 0, c.batchSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.uploadEvents()
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TDBatchConsumer) uploadEvents() error {
|
||||||
|
buffer := c.cacheBuffer[0]
|
||||||
|
|
||||||
|
jsonBytes, err := json.Marshal(buffer)
|
||||||
|
if err == nil {
|
||||||
|
params := parseTime(jsonBytes)
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
statusCode, code, err := c.send(params, len(buffer))
|
||||||
|
if statusCode == 200 {
|
||||||
|
c.cacheBuffer = c.cacheBuffer[1:]
|
||||||
|
switch code {
|
||||||
|
case 0:
|
||||||
|
tdLogInfo("send success: %v", params)
|
||||||
|
return nil
|
||||||
|
case 1, -1:
|
||||||
|
msg := "invalid data format"
|
||||||
|
tdLogError(msg)
|
||||||
|
return fmt.Errorf(msg)
|
||||||
|
case -2:
|
||||||
|
msg := "APP ID doesn't exist"
|
||||||
|
tdLogError(msg)
|
||||||
|
return fmt.Errorf(msg)
|
||||||
|
case -3:
|
||||||
|
msg := "invalid ip transmission"
|
||||||
|
tdLogError(msg)
|
||||||
|
return fmt.Errorf(msg)
|
||||||
|
default:
|
||||||
|
msg := "unknown error"
|
||||||
|
tdLogError(msg)
|
||||||
|
return fmt.Errorf(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
if i == 2 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TDBatchConsumer) FlushAll() error {
|
||||||
|
for c.getCacheLength() > 0 || c.getBufferLength() > 0 {
|
||||||
|
if err := c.Flush(); err != nil {
|
||||||
|
if !strings.Contains(err.Error(), "ThinkingDataError") {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TDBatchConsumer) Close() error {
|
||||||
|
tdLogInfo("batch consumer close")
|
||||||
|
return c.FlushAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TDBatchConsumer) IsStringent() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TDBatchConsumer) send(data string, size int) (statusCode int, code int, err error) {
|
||||||
|
var encodedData string
|
||||||
|
var compressType = "gzip"
|
||||||
|
if c.compress {
|
||||||
|
encodedData, err = encodeData(data)
|
||||||
|
} else {
|
||||||
|
encodedData = data
|
||||||
|
compressType = "none"
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
postData := bytes.NewBufferString(encodedData)
|
||||||
|
|
||||||
|
var resp *http.Response
|
||||||
|
req, _ := http.NewRequest("POST", c.serverUrl, postData)
|
||||||
|
req.Header["appid"] = []string{c.appId}
|
||||||
|
req.Header.Set("user-agent", "ta-go-sdk")
|
||||||
|
req.Header.Set("version", SdkVersion)
|
||||||
|
req.Header.Set("compress", compressType)
|
||||||
|
req.Header["TA-Integration-Type"] = []string{LibName}
|
||||||
|
req.Header["TA-Integration-Version"] = []string{SdkVersion}
|
||||||
|
req.Header["TA-Integration-Count"] = []string{strconv.Itoa(size)}
|
||||||
|
client := &http.Client{Timeout: c.timeout}
|
||||||
|
resp, err = client.Do(req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
body, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
var result struct {
|
||||||
|
Code int
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(body, &result)
|
||||||
|
if err != nil {
|
||||||
|
return resp.StatusCode, 1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.StatusCode, result.Code, nil
|
||||||
|
} else {
|
||||||
|
return resp.StatusCode, -1, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gzip
|
||||||
|
func encodeData(data string) (string, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
gw := gzip.NewWriter(&buf)
|
||||||
|
|
||||||
|
_, err := gw.Write([]byte(data))
|
||||||
|
if err != nil {
|
||||||
|
gw.Close()
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
gw.Close()
|
||||||
|
|
||||||
|
return string(buf.Bytes()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TDBatchConsumer) getBufferLength() int {
|
||||||
|
c.bufferMutex.RLock()
|
||||||
|
defer c.bufferMutex.RUnlock()
|
||||||
|
return len(c.buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TDBatchConsumer) getCacheLength() int {
|
||||||
|
c.cacheMutex.RLock()
|
||||||
|
defer c.cacheMutex.RUnlock()
|
||||||
|
return len(c.cacheBuffer)
|
||||||
|
}
|
||||||
120
src/server/thinkingdata/consumer_debug.go
Normal file
120
src/server/thinkingdata/consumer_debug.go
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
package thinkingdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TDDebugConsumer The data is reported one by one, and when an error occurs, the log will be printed on the console.
|
||||||
|
type TDDebugConsumer struct {
|
||||||
|
serverUrl string // serverUrl
|
||||||
|
appId string // appId
|
||||||
|
writeData bool // is archive to TE
|
||||||
|
deviceId string // be used to debug in TE
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDebugConsumer init TDDebugConsumer
|
||||||
|
func NewDebugConsumer(serverUrl string, appId string) (TDConsumer, error) {
|
||||||
|
return NewDebugConsumerWithWriter(serverUrl, appId, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDebugConsumerWithWriter(serverUrl string, appId string, writeData bool) (TDConsumer, error) {
|
||||||
|
return NewDebugConsumerWithDeviceId(serverUrl, appId, writeData, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDebugConsumerWithDeviceId(serverUrl string, appId string, writeData bool, deviceId string) (TDConsumer, error) {
|
||||||
|
// enable console log
|
||||||
|
SetLogLevel(TDLogLevelDebug)
|
||||||
|
|
||||||
|
if len(serverUrl) <= 0 {
|
||||||
|
msg := fmt.Sprint("ServerUrl not be empty")
|
||||||
|
tdLogError(msg)
|
||||||
|
return nil, errors.New(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := url.Parse(serverUrl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Path = "/data_debug"
|
||||||
|
|
||||||
|
c := &TDDebugConsumer{serverUrl: u.String(), appId: appId, writeData: writeData, deviceId: deviceId}
|
||||||
|
|
||||||
|
tdLogInfo("Mode: debug consumer, appId: %s, serverUrl: %s", c.appId, c.serverUrl)
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TDDebugConsumer) Add(d Data) error {
|
||||||
|
jsonBytes, err := json.Marshal(d)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsonStr string
|
||||||
|
// if properties has includes complex data, SDK need parse time with regular expression
|
||||||
|
if d.IsComplex {
|
||||||
|
jsonStr = parseTime(jsonBytes)
|
||||||
|
} else {
|
||||||
|
jsonStr = string(jsonBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
tdLogInfo("%v", jsonStr)
|
||||||
|
|
||||||
|
return c.send(jsonStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TDDebugConsumer) Flush() error {
|
||||||
|
tdLogInfo("flush data")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TDDebugConsumer) Close() error {
|
||||||
|
tdLogInfo("debug consumer close")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TDDebugConsumer) IsStringent() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TDDebugConsumer) send(data string) error {
|
||||||
|
var dryRun = "0"
|
||||||
|
if !c.writeData {
|
||||||
|
dryRun = "1"
|
||||||
|
}
|
||||||
|
postData := url.Values{"data": {data}, "appid": {c.appId}, "source": {"server"}, "dryRun": {dryRun}}
|
||||||
|
if len(c.deviceId) > 0 {
|
||||||
|
postData.Add("deviceId", c.deviceId)
|
||||||
|
}
|
||||||
|
resp, err := http.PostForm(c.serverUrl, postData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
body, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
result := map[string]interface{}{}
|
||||||
|
err = json.Unmarshal(body, &result)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if uint64(result["errorLevel"].(float64)) != 0 {
|
||||||
|
msg := fmt.Sprintf("send to receiver failed with return content: %s", string(body))
|
||||||
|
tdLogError(msg)
|
||||||
|
return errors.New(msg)
|
||||||
|
} else {
|
||||||
|
tdLogInfo("send success: %v", result)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return errors.New(fmt.Sprintf("Unexpected Status Code: %d", resp.StatusCode))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
256
src/server/thinkingdata/consumer_log.go
Normal file
256
src/server/thinkingdata/consumer_log.go
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
package thinkingdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RotateMode int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultChannelSize = 1000 // channel size
|
||||||
|
ROTATE_DAILY RotateMode = 0 // by the day
|
||||||
|
ROTATE_HOURLY RotateMode = 1 // by the hour
|
||||||
|
)
|
||||||
|
|
||||||
|
// TDLogConsumer write data to file, it works with LogBus
|
||||||
|
type TDLogConsumer struct {
|
||||||
|
directory string // directory of log file
|
||||||
|
dateFormat string // name format of log file
|
||||||
|
fileSize int64 // max size of single log file (MByte)
|
||||||
|
fileNamePrefix string // prefix of log file
|
||||||
|
currentFile *os.File // current file handler
|
||||||
|
wg sync.WaitGroup
|
||||||
|
ch chan []byte
|
||||||
|
mutex *sync.RWMutex
|
||||||
|
sdkClose bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type TDLogConsumerConfig struct {
|
||||||
|
Directory string // directory of log file
|
||||||
|
RotateMode RotateMode // rotate mode of log file
|
||||||
|
FileSize int // max size of single log file (MByte)
|
||||||
|
FileNamePrefix string // prefix of log file
|
||||||
|
ChannelSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogConsumer(directory string, r RotateMode) (TDConsumer, error) {
|
||||||
|
return NewLogConsumerWithFileSize(directory, r, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLogConsumerWithFileSize init TDLogConsumer
|
||||||
|
// directory: directory of log file
|
||||||
|
// r: rotate mode of log file. (in days / hours)
|
||||||
|
// size: max size of single log file (MByte)
|
||||||
|
func NewLogConsumerWithFileSize(directory string, r RotateMode, size int) (TDConsumer, error) {
|
||||||
|
config := TDLogConsumerConfig{
|
||||||
|
Directory: directory,
|
||||||
|
RotateMode: r,
|
||||||
|
FileSize: size,
|
||||||
|
}
|
||||||
|
return NewLogConsumerWithConfig(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogConsumerWithConfig(config TDLogConsumerConfig) (TDConsumer, error) {
|
||||||
|
var df string
|
||||||
|
switch config.RotateMode {
|
||||||
|
case ROTATE_DAILY:
|
||||||
|
df = "2006-01-02"
|
||||||
|
case ROTATE_HOURLY:
|
||||||
|
df = "2006-01-02-15"
|
||||||
|
default:
|
||||||
|
errStr := "unknown rotate mode"
|
||||||
|
tdLogInfo(errStr)
|
||||||
|
return nil, errors.New(errStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
chanSize := DefaultChannelSize
|
||||||
|
if config.ChannelSize > 0 {
|
||||||
|
chanSize = config.ChannelSize
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &TDLogConsumer{
|
||||||
|
directory: config.Directory,
|
||||||
|
dateFormat: df,
|
||||||
|
fileSize: int64(config.FileSize * 1024 * 1024),
|
||||||
|
fileNamePrefix: config.FileNamePrefix,
|
||||||
|
wg: sync.WaitGroup{},
|
||||||
|
ch: make(chan []byte, chanSize),
|
||||||
|
mutex: new(sync.RWMutex),
|
||||||
|
sdkClose: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, c.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TDLogConsumer) Add(d Data) error {
|
||||||
|
var err error = nil
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer func() {
|
||||||
|
c.mutex.Unlock()
|
||||||
|
}()
|
||||||
|
if c.sdkClose {
|
||||||
|
err = errors.New("add event failed, SDK has been closed")
|
||||||
|
tdLogError(err.Error())
|
||||||
|
} else {
|
||||||
|
jsonBytes, jsonErr := json.Marshal(d)
|
||||||
|
if jsonErr != nil {
|
||||||
|
err = jsonErr
|
||||||
|
} else {
|
||||||
|
c.ch <- jsonBytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TDLogConsumer) Flush() error {
|
||||||
|
tdLogInfo("flush data")
|
||||||
|
var err error = nil
|
||||||
|
c.mutex.Lock()
|
||||||
|
if c.currentFile != nil {
|
||||||
|
err = c.currentFile.Sync()
|
||||||
|
}
|
||||||
|
c.mutex.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TDLogConsumer) Close() error {
|
||||||
|
tdLogInfo("log consumer close")
|
||||||
|
|
||||||
|
var err error = nil
|
||||||
|
c.mutex.Lock()
|
||||||
|
if c.sdkClose {
|
||||||
|
err = errors.New("[ThinkingData][error]: SDK has been closed")
|
||||||
|
} else {
|
||||||
|
close(c.ch)
|
||||||
|
c.wg.Wait()
|
||||||
|
if c.currentFile != nil {
|
||||||
|
_ = c.currentFile.Sync()
|
||||||
|
err = c.currentFile.Close()
|
||||||
|
c.currentFile = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.sdkClose = true
|
||||||
|
c.mutex.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TDLogConsumer) IsStringent() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TDLogConsumer) constructFileName(timeStr string, i int) string {
|
||||||
|
fileNamePrefix := ""
|
||||||
|
if len(c.fileNamePrefix) != 0 {
|
||||||
|
fileNamePrefix = c.fileNamePrefix + "."
|
||||||
|
}
|
||||||
|
// is need paging
|
||||||
|
if c.fileSize > 0 {
|
||||||
|
return fmt.Sprintf("%s/%slog.%s_%d", c.directory, fileNamePrefix, timeStr, i)
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf("%s/%slog.%s", c.directory, fileNamePrefix, timeStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TDLogConsumer) init() error {
|
||||||
|
fd, err := c.initLogFile()
|
||||||
|
if err != nil {
|
||||||
|
tdLogError("init log file failed: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.currentFile = fd
|
||||||
|
|
||||||
|
c.wg.Add(1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
c.wg.Done()
|
||||||
|
}()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case rec, ok := <-c.ch:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonStr := parseTime(rec)
|
||||||
|
tdLogInfo("write event data: %s", jsonStr)
|
||||||
|
c.writeToFile(jsonStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
tdLogInfo("Mode: log consumer, log path: " + c.directory)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TDLogConsumer) initLogFile() (*os.File, error) {
|
||||||
|
_, err := os.Stat(c.directory)
|
||||||
|
if err != nil && os.IsNotExist(err) {
|
||||||
|
e := os.MkdirAll(c.directory, os.ModePerm)
|
||||||
|
if e != nil {
|
||||||
|
return nil, e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
timeStr := time.Now().Format(c.dateFormat)
|
||||||
|
return os.OpenFile(c.constructFileName(timeStr, 0), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0664)
|
||||||
|
}
|
||||||
|
|
||||||
|
var logFileIndex = 0
|
||||||
|
|
||||||
|
func (c *TDLogConsumer) writeToFile(str string) {
|
||||||
|
timeStr := time.Now().Format(c.dateFormat)
|
||||||
|
// paging by Rotate Mode and current file size
|
||||||
|
var newName string
|
||||||
|
fName := c.constructFileName(timeStr, logFileIndex)
|
||||||
|
|
||||||
|
if c.currentFile == nil {
|
||||||
|
var openFileErr error
|
||||||
|
c.currentFile, openFileErr = os.OpenFile(fName, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0664)
|
||||||
|
if openFileErr != nil {
|
||||||
|
tdLogInfo("open log file failed: %s\n", openFileErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.currentFile.Name() != fName {
|
||||||
|
newName = fName
|
||||||
|
} else if c.fileSize > 0 {
|
||||||
|
stat, _ := c.currentFile.Stat()
|
||||||
|
if stat.Size() > c.fileSize {
|
||||||
|
logFileIndex++
|
||||||
|
newName = c.constructFileName(timeStr, logFileIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if newName != "" {
|
||||||
|
err := c.currentFile.Close()
|
||||||
|
if err != nil {
|
||||||
|
tdLogInfo("close file failed: %s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.currentFile, err = os.OpenFile(fName, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0664)
|
||||||
|
if err != nil {
|
||||||
|
tdLogInfo("rotate log file failed: %s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err := fmt.Fprintln(c.currentFile, str)
|
||||||
|
if err != nil {
|
||||||
|
tdLogInfo("LoggerWriter(%q): %s\n", c.currentFile.Name(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: please use TDLogConsumer
|
||||||
|
type LogConsumer struct {
|
||||||
|
TDLogConsumer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: please use TDLogConsumerConfig
|
||||||
|
type LogConfig struct {
|
||||||
|
TDLogConsumerConfig
|
||||||
|
}
|
||||||
125
src/server/thinkingdata/td_log.go
Normal file
125
src/server/thinkingdata/td_log.go
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
package thinkingdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SDK_LOG_PREFIX const
|
||||||
|
const SDK_LOG_PREFIX = "[ThinkingData]"
|
||||||
|
|
||||||
|
var logInstance TDLogger
|
||||||
|
|
||||||
|
type TDLogLevel int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
TDLogLevelOff TDLogLevel = 1
|
||||||
|
TDLogLevelError TDLogLevel = 2
|
||||||
|
TDLogLevelWarning TDLogLevel = 3
|
||||||
|
TDLogLevelInfo TDLogLevel = 4
|
||||||
|
TDLogLevelDebug TDLogLevel = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
// default is TDLogLevelOff
|
||||||
|
var currentLogLevel = TDLogLevelOff
|
||||||
|
|
||||||
|
// TDLogger User-defined log classes must comply with interface
|
||||||
|
type TDLogger interface {
|
||||||
|
Print(message string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLogLevel Set the log output level
|
||||||
|
func SetLogLevel(level TDLogLevel) {
|
||||||
|
if level < TDLogLevelOff || level > TDLogLevelDebug {
|
||||||
|
fmt.Println(SDK_LOG_PREFIX + "log type error")
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
currentLogLevel = level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCustomLogger Set a custom log input class, usually you don't need to set it up.
|
||||||
|
func SetCustomLogger(logger TDLogger) {
|
||||||
|
if logger != nil {
|
||||||
|
logInstance = logger
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tdLog(level TDLogLevel, format string, v ...interface{}) {
|
||||||
|
if level > currentLogLevel {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var modeStr string
|
||||||
|
switch level {
|
||||||
|
case TDLogLevelError:
|
||||||
|
modeStr = "[Error] "
|
||||||
|
break
|
||||||
|
case TDLogLevelWarning:
|
||||||
|
modeStr = "[Warning] "
|
||||||
|
break
|
||||||
|
case TDLogLevelInfo:
|
||||||
|
modeStr = "[Info] "
|
||||||
|
break
|
||||||
|
case TDLogLevelDebug:
|
||||||
|
modeStr = "[Debug] "
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
modeStr = "[Info] "
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if logInstance != nil {
|
||||||
|
msg := fmt.Sprintf(SDK_LOG_PREFIX+modeStr+format+"\n", v...)
|
||||||
|
logInstance.Print(msg)
|
||||||
|
} else {
|
||||||
|
logTime := fmt.Sprintf("[%v]", time.Now().Format("2006-01-02 15:04:05.000"))
|
||||||
|
fmt.Printf(logTime+SDK_LOG_PREFIX+modeStr+format+"\n", v...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tdLogDebug(format string, v ...interface{}) {
|
||||||
|
tdLog(TDLogLevelDebug, format, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tdLogInfo(format string, v ...interface{}) {
|
||||||
|
tdLog(TDLogLevelInfo, format, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tdLogError(format string, v ...interface{}) {
|
||||||
|
tdLog(TDLogLevelError, format, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tdLogWarning(format string, v ...interface{}) {
|
||||||
|
tdLog(TDLogLevelWarning, format, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: please use thinkingdata.SetLogLevel(thinkingdata.TDLogLevelOff)
|
||||||
|
type LogType int32
|
||||||
|
|
||||||
|
// Deprecated: please use thinkingdata.SetLogLevel(thinkingdata.TDLogLevelOff)
|
||||||
|
const (
|
||||||
|
LoggerTypeOff LogType = 1 << 0 // disable log
|
||||||
|
LoggerTypePrint LogType = 1 << 1 // print on console
|
||||||
|
LoggerTypeWriteFile LogType = 1 << 2 // print to file
|
||||||
|
LoggerTypePrintAndWriteFile = LoggerTypePrint | LoggerTypeWriteFile // print both on console and file
|
||||||
|
)
|
||||||
|
|
||||||
|
// Deprecated: please use thinkingdata.SetLogLevel(thinkingdata.TDLogLevelOff)
|
||||||
|
type LoggerConfig struct {
|
||||||
|
Type LogType
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: please use thinkingdata.SetLogLevel(thinkingdata.TDLogLevelOff)
|
||||||
|
func SetLoggerConfig(config LoggerConfig) {
|
||||||
|
if config.Type < LoggerTypeOff || config.Type > LoggerTypePrintAndWriteFile {
|
||||||
|
fmt.Println(SDK_LOG_PREFIX + "log type error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if config.Type&LoggerTypeOff == LoggerTypeOff {
|
||||||
|
currentLogLevel = TDLogLevelOff
|
||||||
|
} else {
|
||||||
|
currentLogLevel = TDLogLevelInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
305
src/server/thinkingdata/thinkingdata.go
Normal file
305
src/server/thinkingdata/thinkingdata.go
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
package thinkingdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Track = "track"
|
||||||
|
TrackUpdate = "track_update"
|
||||||
|
TrackOverwrite = "track_overwrite"
|
||||||
|
UserSet = "user_set"
|
||||||
|
UserUnset = "user_unset"
|
||||||
|
UserSetOnce = "user_setOnce"
|
||||||
|
UserAdd = "user_add"
|
||||||
|
UserAppend = "user_append"
|
||||||
|
UserUniqAppend = "user_uniq_append"
|
||||||
|
UserDel = "user_del"
|
||||||
|
|
||||||
|
SdkVersion = "2.0.3"
|
||||||
|
LibName = "Golang"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Data struct {
|
||||||
|
IsComplex bool `json:"-"` // properties are nested or not
|
||||||
|
AccountId string `json:"#account_id,omitempty"`
|
||||||
|
DistinctId string `json:"#distinct_id,omitempty"`
|
||||||
|
Type string `json:"#type"`
|
||||||
|
Time string `json:"#time"`
|
||||||
|
EventName string `json:"#event_name,omitempty"`
|
||||||
|
EventId string `json:"#event_id,omitempty"`
|
||||||
|
FirstCheckId string `json:"#first_check_id,omitempty"`
|
||||||
|
Ip string `json:"#ip,omitempty"`
|
||||||
|
UUID string `json:"#uuid,omitempty"`
|
||||||
|
AppId string `json:"#app_id,omitempty"`
|
||||||
|
Properties map[string]interface{} `json:"properties"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TDConsumer define operation interface
|
||||||
|
type TDConsumer interface {
|
||||||
|
Add(d Data) error
|
||||||
|
Flush() error
|
||||||
|
Close() error
|
||||||
|
IsStringent() bool // check data or not.
|
||||||
|
}
|
||||||
|
|
||||||
|
type TDAnalytics struct {
|
||||||
|
consumer TDConsumer
|
||||||
|
superProperties map[string]interface{}
|
||||||
|
mutex *sync.RWMutex
|
||||||
|
dynamicSuperProperties func() map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New init SDK
|
||||||
|
func New(c TDConsumer) TDAnalytics {
|
||||||
|
tdLogInfo("init SDK success")
|
||||||
|
return TDAnalytics{
|
||||||
|
consumer: c,
|
||||||
|
superProperties: make(map[string]interface{}),
|
||||||
|
mutex: new(sync.RWMutex),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSuperProperties get common properties
|
||||||
|
func (ta *TDAnalytics) GetSuperProperties() map[string]interface{} {
|
||||||
|
result := make(map[string]interface{})
|
||||||
|
ta.mutex.Lock()
|
||||||
|
mergeProperties(result, ta.superProperties)
|
||||||
|
ta.mutex.Unlock()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSuperProperties set common properties
|
||||||
|
func (ta *TDAnalytics) SetSuperProperties(superProperties map[string]interface{}) {
|
||||||
|
ta.mutex.Lock()
|
||||||
|
mergeProperties(ta.superProperties, superProperties)
|
||||||
|
ta.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearSuperProperties clear common properties
|
||||||
|
func (ta *TDAnalytics) ClearSuperProperties() {
|
||||||
|
ta.mutex.Lock()
|
||||||
|
ta.superProperties = make(map[string]interface{})
|
||||||
|
ta.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDynamicSuperProperties set common properties dynamically.
|
||||||
|
// not recommend to add the operation which with a lot of computation
|
||||||
|
func (ta *TDAnalytics) SetDynamicSuperProperties(action func() map[string]interface{}) {
|
||||||
|
ta.mutex.Lock()
|
||||||
|
ta.dynamicSuperProperties = action
|
||||||
|
ta.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDynamicSuperProperties dynamic common properties
|
||||||
|
func (ta *TDAnalytics) GetDynamicSuperProperties() map[string]interface{} {
|
||||||
|
result := make(map[string]interface{})
|
||||||
|
ta.mutex.RLock()
|
||||||
|
if ta.dynamicSuperProperties != nil {
|
||||||
|
mergeProperties(result, ta.dynamicSuperProperties())
|
||||||
|
}
|
||||||
|
ta.mutex.RUnlock()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track report ordinary event
|
||||||
|
func (ta *TDAnalytics) Track(accountId, distinctId, eventName string, properties map[string]interface{}) error {
|
||||||
|
return ta.track(accountId, distinctId, Track, eventName, "", properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrackFirst report first event
|
||||||
|
func (ta *TDAnalytics) TrackFirst(accountId, distinctId, eventName, firstCheckId string, properties map[string]interface{}) error {
|
||||||
|
if len(firstCheckId) == 0 {
|
||||||
|
msg := "the 'firstCheckId' must be provided"
|
||||||
|
tdLogInfo(msg)
|
||||||
|
return errors.New(msg)
|
||||||
|
}
|
||||||
|
p := make(map[string]interface{})
|
||||||
|
mergeProperties(p, properties)
|
||||||
|
p["#first_check_id"] = firstCheckId
|
||||||
|
return ta.track(accountId, distinctId, Track, eventName, "", p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrackUpdate report updatable event
|
||||||
|
func (ta *TDAnalytics) TrackUpdate(accountId, distinctId, eventName, eventId string, properties map[string]interface{}) error {
|
||||||
|
return ta.track(accountId, distinctId, TrackUpdate, eventName, eventId, properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrackOverwrite report overridable event
|
||||||
|
func (ta *TDAnalytics) TrackOverwrite(accountId, distinctId, eventName, eventId string, properties map[string]interface{}) error {
|
||||||
|
return ta.track(accountId, distinctId, TrackOverwrite, eventName, eventId, properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ta *TDAnalytics) track(accountId, distinctId, dataType, eventName, eventId string, properties map[string]interface{}) error {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
tdLogError("%+v\ndata: %+v", r, properties)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if len(eventName) == 0 {
|
||||||
|
msg := "the event name must be provided"
|
||||||
|
tdLogError(msg)
|
||||||
|
return errors.New(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// eventId not be null unless eventType is equal Track.
|
||||||
|
if len(eventId) == 0 && dataType != Track {
|
||||||
|
msg := "the event id must be provided"
|
||||||
|
tdLogError(msg)
|
||||||
|
return errors.New(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
p := ta.GetSuperProperties()
|
||||||
|
dynamicSuperProperties := ta.GetDynamicSuperProperties()
|
||||||
|
|
||||||
|
mergeProperties(p, dynamicSuperProperties)
|
||||||
|
// preset properties has the highest priority
|
||||||
|
p["#lib"] = LibName
|
||||||
|
p["#lib_version"] = SdkVersion
|
||||||
|
// custom properties
|
||||||
|
mergeProperties(p, properties)
|
||||||
|
|
||||||
|
return ta.add(accountId, distinctId, dataType, eventName, eventId, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserSet set user properties. would overwrite existing names.
|
||||||
|
func (ta *TDAnalytics) UserSet(accountId string, distinctId string, properties map[string]interface{}) error {
|
||||||
|
return ta.user(accountId, distinctId, UserSet, properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserUnset clear the user properties of users.
|
||||||
|
func (ta *TDAnalytics) UserUnset(accountId string, distinctId string, s []string) error {
|
||||||
|
if len(s) == 0 {
|
||||||
|
msg := "invalid params for UserUnset: keys is nil"
|
||||||
|
tdLogInfo(msg)
|
||||||
|
return errors.New(msg)
|
||||||
|
}
|
||||||
|
prop := make(map[string]interface{})
|
||||||
|
for _, v := range s {
|
||||||
|
prop[v] = 0
|
||||||
|
}
|
||||||
|
return ta.user(accountId, distinctId, UserUnset, prop)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ta *TDAnalytics) UserUnsetWithProperties(accountId string, distinctId string, properties map[string]interface{}) error {
|
||||||
|
if len(properties) == 0 {
|
||||||
|
msg := "invalid params for UserUnset: properties is nil"
|
||||||
|
tdLogInfo(msg)
|
||||||
|
return errors.New(msg)
|
||||||
|
}
|
||||||
|
return ta.user(accountId, distinctId, UserUnset, properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserSetOnce set user properties, If such property had been set before, this message would be neglected.
|
||||||
|
func (ta *TDAnalytics) UserSetOnce(accountId string, distinctId string, properties map[string]interface{}) error {
|
||||||
|
return ta.user(accountId, distinctId, UserSetOnce, properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserAdd to accumulate operations against the property.
|
||||||
|
func (ta *TDAnalytics) UserAdd(accountId string, distinctId string, properties map[string]interface{}) error {
|
||||||
|
return ta.user(accountId, distinctId, UserAdd, properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserAppend to add user properties of array type.
|
||||||
|
func (ta *TDAnalytics) UserAppend(accountId string, distinctId string, properties map[string]interface{}) error {
|
||||||
|
return ta.user(accountId, distinctId, UserAppend, properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserUniqAppend append user properties to array type by unique.
|
||||||
|
func (ta *TDAnalytics) UserUniqAppend(accountId string, distinctId string, properties map[string]interface{}) error {
|
||||||
|
return ta.user(accountId, distinctId, UserUniqAppend, properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserDelete delete a user, This operation cannot be undone.
|
||||||
|
func (ta *TDAnalytics) UserDelete(accountId string, distinctId string) error {
|
||||||
|
return ta.user(accountId, distinctId, UserDel, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserDeleteWithProperties delete a user, This operation cannot be undone.
|
||||||
|
func (ta *TDAnalytics) UserDeleteWithProperties(accountId string, distinctId string, properties map[string]interface{}) error {
|
||||||
|
return ta.user(accountId, distinctId, UserDel, properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ta *TDAnalytics) user(accountId, distinctId, dataType string, properties map[string]interface{}) error {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
tdLogError("%+v\ndata: %+v", r, properties)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if properties == nil && dataType != UserDel {
|
||||||
|
msg := "invalid params for " + dataType + ": properties is nil"
|
||||||
|
tdLogError(msg)
|
||||||
|
return errors.New(msg)
|
||||||
|
}
|
||||||
|
p := make(map[string]interface{})
|
||||||
|
mergeProperties(p, properties)
|
||||||
|
return ta.add(accountId, distinctId, dataType, "", "", p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush report data immediately.
|
||||||
|
func (ta *TDAnalytics) Flush() error {
|
||||||
|
return ta.consumer.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close and exit sdk
|
||||||
|
func (ta *TDAnalytics) Close() error {
|
||||||
|
err := ta.consumer.Close()
|
||||||
|
tdLogInfo("SDK close")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ta *TDAnalytics) add(accountId, distinctId, dataType, eventName, eventId string, properties map[string]interface{}) error {
|
||||||
|
if len(accountId) == 0 && len(distinctId) == 0 {
|
||||||
|
msg := "invalid parameters: account_id and distinct_id cannot be empty at the same time"
|
||||||
|
tdLogError(msg)
|
||||||
|
return errors.New(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get "#ip" value in properties, empty string will be return when not found.
|
||||||
|
ip := extractStringProperty(properties, "#ip")
|
||||||
|
|
||||||
|
// get "#app_id" value in properties, empty string will be return when not found.
|
||||||
|
appId := extractStringProperty(properties, "#app_id")
|
||||||
|
|
||||||
|
// get "#time" value in properties, empty string will be return when not found.
|
||||||
|
eventTime := extractTime(properties)
|
||||||
|
|
||||||
|
firstCheckId := extractStringProperty(properties, "#first_check_id")
|
||||||
|
|
||||||
|
// get "#uuid" value in properties, empty string will be return when not found.
|
||||||
|
uuid := extractStringProperty(properties, "#uuid")
|
||||||
|
if len(uuid) == 0 {
|
||||||
|
uuid = generateUUID()
|
||||||
|
}
|
||||||
|
|
||||||
|
data := Data{
|
||||||
|
AccountId: accountId,
|
||||||
|
DistinctId: distinctId,
|
||||||
|
Type: dataType,
|
||||||
|
Time: eventTime,
|
||||||
|
EventName: eventName,
|
||||||
|
EventId: eventId,
|
||||||
|
FirstCheckId: firstCheckId,
|
||||||
|
Ip: ip,
|
||||||
|
UUID: uuid,
|
||||||
|
Properties: properties,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(appId) > 0 {
|
||||||
|
data.AppId = appId
|
||||||
|
}
|
||||||
|
|
||||||
|
err := formatProperties(&data, ta)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ta.consumer.Add(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: please use TDConsumer
|
||||||
|
type Consumer interface {
|
||||||
|
TDConsumer
|
||||||
|
}
|
||||||
143
src/server/thinkingdata/utils.go
Normal file
143
src/server/thinkingdata/utils.go
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
package thinkingdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DATE_FORMAT = "2006-01-02 15:04:05.000"
|
||||||
|
KEY_PATTERN = "^[a-zA-Z#][A-Za-z0-9_]{0,49}$"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A string of 50 letters and digits that starts with '#' or a letter
|
||||||
|
var keyPattern, _ = regexp.Compile(KEY_PATTERN)
|
||||||
|
|
||||||
|
func mergeProperties(target, source map[string]interface{}) {
|
||||||
|
for k, v := range source {
|
||||||
|
target[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractTime(p map[string]interface{}) string {
|
||||||
|
if t, ok := p["#time"]; ok {
|
||||||
|
delete(p, "#time")
|
||||||
|
switch v := t.(type) {
|
||||||
|
case string:
|
||||||
|
return v
|
||||||
|
case time.Time:
|
||||||
|
return v.Format(DATE_FORMAT)
|
||||||
|
default:
|
||||||
|
return time.Now().Format(DATE_FORMAT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Now().Format(DATE_FORMAT)
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractStringProperty(p map[string]interface{}, key string) string {
|
||||||
|
if t, ok := p[key]; ok {
|
||||||
|
delete(p, key)
|
||||||
|
v, ok := t.(string)
|
||||||
|
if !ok {
|
||||||
|
fmt.Fprintln(os.Stderr, "Invalid data type for "+key)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNotNumber(v interface{}) bool {
|
||||||
|
switch v.(type) {
|
||||||
|
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
|
||||||
|
case float32, float64:
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatProperties(d *Data, ta *TDAnalytics) error {
|
||||||
|
|
||||||
|
if d.EventName != "" {
|
||||||
|
matched := checkPattern([]byte(d.EventName))
|
||||||
|
if !matched {
|
||||||
|
msg := "invalid event name: " + d.EventName
|
||||||
|
tdLogInfo(msg)
|
||||||
|
return errors.New(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Properties != nil {
|
||||||
|
for k, v := range d.Properties {
|
||||||
|
if ta.consumer.IsStringent() {
|
||||||
|
isMatch := checkPattern([]byte(k))
|
||||||
|
if !isMatch {
|
||||||
|
msg := "invalid property key: " + k
|
||||||
|
tdLogInfo(msg)
|
||||||
|
return errors.New(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Type == UserAdd && isNotNumber(v) {
|
||||||
|
msg := "invalid property value: only numbers is supported by UserAdd"
|
||||||
|
tdLogInfo(msg)
|
||||||
|
return errors.New(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check value
|
||||||
|
switch v.(type) {
|
||||||
|
case int:
|
||||||
|
case bool:
|
||||||
|
case float64:
|
||||||
|
case string:
|
||||||
|
case time.Time:
|
||||||
|
d.Properties[k] = v.(time.Time).Format(DATE_FORMAT)
|
||||||
|
case []string:
|
||||||
|
d.IsComplex = true
|
||||||
|
default:
|
||||||
|
d.IsComplex = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNotArrayOrSlice(v interface{}) bool {
|
||||||
|
typeOf := reflect.TypeOf(v)
|
||||||
|
switch typeOf.Kind() {
|
||||||
|
case reflect.Array:
|
||||||
|
case reflect.Slice:
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkPattern(name []byte) bool {
|
||||||
|
return keyPattern.Match(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTime(input []byte) string {
|
||||||
|
var re = regexp.MustCompile(`"((\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})(?:\.(\d{3}))\d*)(Z|[\+-]\d{2}:\d{2})"`)
|
||||||
|
var substitution = "\"$2 $3.$4\""
|
||||||
|
|
||||||
|
for re.Match(input) {
|
||||||
|
input = re.ReplaceAll(input, []byte(substitution))
|
||||||
|
}
|
||||||
|
return string(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateUUID() string {
|
||||||
|
newUUID, err := uuid.NewUUID()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return newUUID.String()
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user